O que são testes

Ao escrever um software, é possível confirmar se ele funciona corretamente por meio de testes. O teste pode ser amplamente definido como o processo de execução de software de maneiras específicas para garantir que ele se comporte conforme o esperado.

Com testes bem-sucedidos, você tem a certeza de que, à medida que adiciona novos códigos, recursos ou até faz upgrade das dependências, o software que já criou continua funcionando da maneira esperada. Os testes também ajudam a proteger o software contra cenários improváveis ou entradas inesperadas.

Alguns exemplos de comportamento na Web que podem ser testados incluem:

  • Garantir que o recurso de um site funcione corretamente quando um botão é clicado.
  • Confirmar se uma função complexa produz os resultados corretos.
  • Concluir uma ação que exige login do usuário.
  • Verificar se um formulário relata corretamente um erro quando dados malformados são inseridos.
  • Garantir que um app da Web complexo continue funcionando quando um usuário tem largura de banda extremamente baixa ou fica off-line.

Comparação entre testes automatizados e manuais

É possível testar seu software de duas maneiras gerais: testes automatizados e manuais.

O teste manual envolve pessoas executando o software diretamente, por exemplo, carregando um site no navegador e confirmando que ele se comporta conforme o esperado. É fácil criar ou definir testes manuais. Por exemplo, o site consegue carregar? Você consegue realizar essas ações? No entanto, cada execução custa muito tempo para o ser humano. Embora os humanos sejam muito criativos, que podem permitir um tipo de teste conhecido como testes exploratórios, ainda não notamos falhas ou inconsistências, especialmente quando fazemos a mesma tarefa muitas vezes.

Os testes automatizados são qualquer processo que permite que testes sejam codificados e executados repetidamente por um computador para confirmar o comportamento pretendido do software sem que uma pessoa execute etapas repetidas, como configurar ou verificar os resultados. É importante ressaltar que, depois que o teste automatizado é configurado, ele pode ser executado com frequência. Essa ainda é uma definição muito ampla, e é importante notar que os testes automatizados assumem todos os tipos de formas. A maior parte deste curso trata de testes automatizados.

O teste manual tem seu lugar, geralmente como um precursor da criação de testes automatizados, mas também quando os testes automatizados se tornam muito não confiáveis, amplos em escopo ou difíceis de controlar.

Os princípios básicos por meio de um exemplo

Para nós, como desenvolvedores da Web que escrevem JavaScript ou linguagens relacionadas, um teste automatizado conciso pode ser um script como este que você executa todos os dias, talvez por meio do Node ou carregando-o em um navegador:

import { fibonacci } from "../src/math.js";

if (fibonacci(0) !== 0) {
  throw new Error("Invalid 0th fibonacci result");
}
const fib13 = fibonacci(13);
if (fib13 !== 233) {
  throw new Error("Invalid 13th fibonacci result, was=${fib13} wanted=233");
}

Este é um exemplo simplificado que mostra os seguintes insights:

  • Isso é um teste, porque executa algum software (a função Fibonacci, em inglês) e garante que o comportamento funcione da maneira pretendida, verificando os resultados em relação aos valores esperados. Se o comportamento não estiver correto, ele vai causar um erro, que o JavaScript expressa gerando uma Error.

  • Mesmo que você esteja executando esse script manualmente no terminal ou no navegador, ele ainda é um teste automatizado porque pode ser executado repetidamente sem que você precise realizar etapas individuais. A próxima página, em que os testes são executados, explica mais.

  • Mesmo que o teste não use nenhuma biblioteca, porque um JavaScript que pode ser executado em qualquer lugar, ele ainda é um teste. Existem muitas ferramentas que podem ajudar você a criar testes, incluindo as que serão abordadas mais adiante neste curso, mas todas elas ainda funcionam com base no princípio fundamental de causar um erro se algo der errado.

Como testar bibliotecas na prática

A maioria das bibliotecas ou dos frameworks de teste integrados oferece dois primitivos principais que facilitam a criação de testes: declarações e uma maneira de definir testes independentes. Isso será abordado em detalhes na próxima seção, declarações e outros primitivos. No entanto, é importante lembrar que quase todos os testes que você vê ou programa acabarão usando esses tipos de primitivos.

Declarações são uma maneira de combinar a verificação de um resultado e causar um erro se algo der errado. Por exemplo, é possível tornar o teste anterior mais conciso introduzindo assert:

import { fibonacci } from "../src/math.js";
import { assert } from "a-made-up-testing-library";

assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");

É possível melhorar ainda mais esse teste definindo testes independentes, opcionalmente agrupados em conjuntos. O pacote a seguir testa de forma independente a função de Fibonacci e a função Catalan (links em inglês):

import { fibonacci, catalan } from "../src/math.js";
import { assert, test, suite } from "a-made-up-testing-library";

suite("math tests", () => {
  test("fibonacci function", () => {
    assert.equal(fibonacci(0), 0, "Invalid 0th fibonacci result");
    assert.equal(fibonacci(13), 233, "Invalid 13th fibonacci result");
  });
  test("relationship between sequences", () => {
    const numberToCheck = 4;
    const fib = fibonacci(numberToCheck);
    const cat = catalan(numberToCheck);
    assert.isAbove(fib, cat);
  });
});

Neste contexto de teste de software, teste, como um substantivo, refere-se a um caso de teste: um cenário único, independente e endereçável, como a "relação entre sequências" no exemplo anterior.

Os testes nomeados individualmente são úteis para as seguintes tarefas, entre outras:

  • Determinar o sucesso ou a falha de um teste ao longo do tempo.
  • Destacar um bug ou cenário pelo nome para que você possa testar mais facilmente se o cenário foi resolvido.
  • Executar alguns testes independentemente de outros, como por meio de um filtro glob.

Uma maneira de pensar nos casos de teste é usar os "três As" do teste de unidade: organizar, agir e declarar. Em sua essência, cada caso de teste:

  • Organize alguns valores ou estados (eles podem ser apenas dados de entrada codificados).
  • Realizar uma ação, como chamar um método.
  • Declare os valores de saída ou o estado atualizado (usando assert).

A escala dos testes

Os exemplos de código na seção anterior descrevem um teste de unidade porque testam partes menores do software, geralmente se concentrando em um único arquivo e, neste caso, apenas na saída de uma única função. A complexidade do teste aumenta à medida que você considera o código de vários arquivos, componentes ou até mesmo diferentes sistemas interconectados (às vezes fora do seu controle, como um serviço de rede ou o comportamento de uma dependência externa). Por isso, os tipos de teste geralmente são nomeados com base no escopo ou escala.

Além dos testes de unidade, alguns exemplos de outros tipos incluem testes de componentes, visuais e de integração. Nenhum desses nomes tem definições rigorosas e eles podem ter significados diferentes dependendo da sua base de código. Portanto, lembre-se de usá-los como um guia e criar definições que funcionem para você. Por exemplo, o que é um componente em teste no sistema? Para desenvolvedores do React, isso pode ser literalmente associado a um "componente React", mas pode ter um significado diferente para desenvolvedores em outros contextos.

A escala de um teste individual pode colocá-lo em um conceito geralmente conhecido como "pirâmide de teste", o que pode ser uma boa regra geral sobre o que um teste verifica e como ele é executado.

A pirâmide de teste, com testes de ponta a ponta (E2E) na parte superior, testes de integração no meio e testes de unidade na parte inferior.
A pirâmide de teste.

Essa ideia foi iterada, e várias outras formas foram conhecidas, como o diamante ou o cone de gelo de teste. Suas prioridades de gravação de testes provavelmente serão exclusivas da sua base de código. No entanto, um recurso comum é que testes mais simples, como testes de unidade, tendem a ser mais rápidos de executar, mais fáceis de programar (ou seja, você terá mais deles) e testam um escopo limitado, enquanto testes complexos como testes completos são difíceis de programar, mas podem testar um escopo mais amplo. Na verdade, a camada superior de muitas "formas" de teste tende a ser o teste manual, porque algumas interações do usuário são muito complexas para serem codificadas em um teste automatizado.

Vamos expandir esses tipos em tipos de testes automatizados.

Teste seu conhecimento

Quais primitivos a maioria das bibliotecas e frameworks de teste fornece?

Um serviço de execução que usa um provedor de nuvem.
Alguns executores baseados em navegador oferecem uma maneira de terceirizar os testes, mas esse não é um recurso normal das bibliotecas de teste.
Declarações que geram exceções quando não são atendidas.
Embora você possa gerar um erro para falhar em um teste, assert() e as variações dele tendem a ser incluídos, porque facilitam a criação de verificações.
Uma maneira de categorizar testes na pirâmide de teste.
Não há uma maneira padrão de fazer isso. É possível prefixar os nomes dos testes ou colocá-los em arquivos diferentes, mas a categorização não está integrada à maioria dos frameworks de teste.
A capacidade de definir testes independentes por função.
O método test() está incluído em quase todos os executores de teste. Ela é importante porque o código de teste não é executado no nível superior de um arquivo, o que permite que o executor de testes trate cada caso de teste como uma unidade independente.