Pular para o conteúdo
Quiz
← Início

Testes

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Imagine que você construiu um robô de brinquedo. Antes de dar o robô a um amigo, você aperta todos os botões para ver se ele anda, fala e vira a cabeça. Você está testando o robô. Um em software é a mesma ideia: um pedacinho de código que usa o seu programa e diz 'sim, está certo' ou 'não, está errado'.

Por que isso importa? Programas mudam o tempo todo. Cada mudança pode consertar uma coisa e quebrar outra bem longe. Se um computador consegue conferir tudo de novo em poucos segundos, as pessoas não têm medo de mexer no código. Apps, bancos, jogos e o software dentro dos carros só podem ser melhorados com segurança porque muitos testes vigiam tudo, todos os dias.

Neste resumo você vai aprender: os diferentes tamanhos de teste e por que queremos muitos pequenos e poucos grandes, como manter um teste sozinho e limpo, como usar ajudantes de mentira chamados dublês de teste, o ritmo vermelho-verde-refatorar do desenvolvimento guiado por testes, como testar peças trabalhando juntas e um app inteiro no navegador, verificações rápidas de saúde e verificações contra bugs antigos, como medir se os seus testes são bons, como o computador pode inventar casos de teste para você, por que alguns testes falham por sorte, como escolher bons valores de teste e como os testes rodam sozinhos toda vez que alguém compartilha código. No fim, você estará pronto para o quiz.

A visão geral: para que serve um teste?

Um teste tem três partes. Primeiro você prepara uma situação. Depois você faz a coisa que quer verificar. Por fim você compara o resultado com o que esperava. Se o resultado for diferente, o teste falha e um humano é avisado.

Palavras que você vai encontrar

  • Defeito (bug): um erro no código que faz o programa se comportar de modo errado.
  • Suíte de testes: um grupo de testes que rodam juntos, como uma prova inteira feita de muitas questões.
  • Passar e falhar: um teste passa quando o resultado é o esperado e falha quando não é. Muitas ferramentas mostram passar em verde e falhar em vermelho.
  • Verificação: conferir se o software faz o que a sua descrição escrita (a especificação) diz. A pergunta é: construímos a coisa direito?
  • Validação: conferir se o software é o que o cliente realmente precisa. A pergunta é: construímos a coisa certa? Um programa pode passar na primeira conferência e falhar na segunda, por exemplo quando a própria descrição estava errada.
  • Teste funcional: confere o que o sistema faz, por exemplo 'para esta entrada, esta saída'.
  • Teste não funcional: confere como o sistema se comporta enquanto faz isso, por exemplo a rapidez, quantos usuários aguenta, a segurança e a facilidade de uso.
↑ Voltar ao sumário

Níveis de teste e a pirâmide de testes

Pense em verificar um carrinho de brinquedo. Você pode olhar só uma roda. Pode olhar a roda e o eixo juntos. Ou pode colocar o carrinho inteiro no chão e empurrar. Cada verificação olha um pedaço maior. Com os testes é igual, e chamamos cada tamanho de nível de teste.

Os níveis, do pequeno ao grande
NívelO que rodaVelocidadeSe falhar, onde está o bug?
UnitárioUma peça pequena (uma função ou uma classe), sozinhaMuito rápidoFácil de achar: poucas linhas rodaram
IntegraçãoDuas ou mais peças trabalhando juntas, como o código mais um banco de dados realMédioNa ligação entre as peças
Sistema ou ponta a ponta (end-to-end)A aplicação inteira, usada como uma pessoa usariaLentoEm qualquer lugar: o teste só diz que algo quebrou
AceitaçãoO cliente decide se o produto é bom o bastanteO mais lentoNão é sobre bugs: é sobre a necessidade do cliente

A pirâmide de testes é um conselho sobre quantos testes de cada nível escrever: muitos testes unitários na base larga, menos testes de integração no meio e pouquíssimos testes de ponta a ponta no topo. O motivo é o custo. Quanto mais alto o nível, mais lento, mais frágil e mais caro é cada teste.

      a pirâmide                  o cone de sorvete
         /\                      ___________________
        /e2e\   poucos           \ muitos e2e+manual /
       /------\                   \ poucos integr. /
      / integr.\  alguns           \ pouquíssimos /
     /----------\                  \ unitários  /
    / unitários  \  muitos          \__________/
   /--------------\         lenta, instável, difícil de depurar
A pirâmide e a sua versão de cabeça para baixo

A versão de cabeça para baixo é um erro conhecido, chamado de cone de sorvete ou pirâmide invertida. Quase toda a verificação é feita por testes de ponta a ponta lentos e por testes manuais. A suíte demora, falha por motivos que nada têm a ver com o código e não diz onde está o bug. O remédio é empurrar cada verificação para baixo, até o nível mais baixo que consegue enxergar o comportamento.

Custo típico de um teste (milissegundos, medido no mini-projeto test-pyramid deste repositório)
unitário
3.6 ms
integração
7.6 ms
ponta a ponta
690 ms

A aritmética da pirâmide

Suponha uma suíte com 400 testes unitários de 5 ms, 50 testes de integração de 200 ms e 10 testes de ponta a ponta de 6 segundos, rodando um depois do outro. O tempo de cada nível é o número de testes vezes a duração de um teste.

Segundos gastos por cada nível dessa suíte
400 testes unitários
2 s
50 testes de integração
10 s
10 testes de ponta a ponta
60 s

A suíte inteira leva 72 segundos, e os 10 testes lentos (uma pequena parte de todos os testes) usam 60 deles, cerca de 83% do tempo. Por isso não queremos um topo largo.

Qual nível enxerga qual bug?

Um teste só encontra bugs que estão no caminho que ele executa. Um erro numa regra de preço escrita como função simples é visto por um teste unitário. Um nome de coluna errado entre o código e o banco só é visto quando código e banco rodam juntos. Um botão cujo clique está ligado ao elemento errado da página só é visto por um teste que abre a página, porque o código do servidor está perfeito.

As três etapas de Sommerville

  • Teste de desenvolvimento: feito por quem constrói o sistema, para procurar defeitos (testes unitários, de componente e de sistema).
  • Teste de release (de versão): uma equipe separada testa uma versão completa para mostrar que ela atende aos requisitos e é boa o bastante para uso.
  • Teste de usuário: usuários ou clientes de verdade experimentam o sistema. No teste alfa eles testam junto com a equipe. No teste beta uma versão inicial vai para voluntários que a usam no ambiente real deles. No teste de aceitação o cliente decide se aceita o sistema.
↑ Voltar ao sumário

Testes unitários e isolamento

Um teste unitário verifica uma peça pequena do programa, como uma função, sozinha. 'Sozinha' é a palavra-chave. Para saber se uma lâmpada funciona, você a testa sozinha, não dentro da casa inteira.

Um bom teste unitário é rápido (milissegundos), isolado (não depende de outros testes), repetível (mesmo resultado sempre) e autoverificável (o próprio teste diz passou ou falhou com asserções, sem que uma pessoa leia a saída e compare). Uma suíte autoverificável roda milhares de testes e você só olha os que falharam.

Arrange, Act, Assert

test("um cupom de 10% baixa 200 para 180", () => {
	// Arrange: preparar a situação
	const cart = new Cart([{ price: 200 }]);
	// Act: fazer a única coisa que está sendo testada
	const total = cart.totalWith("TEN");
	// Assert: comparar com o esperado
	expect(total).toBe(180);
});
Um teste, três partes visíveis

Mantenha um único Act por teste. Assim, quando o teste falha, você sabe exatamente qual comportamento está errado.

A fixture e a família xUnit

A é o palco onde o teste atua. Na família xUnit de ferramentas (JUnit, pytest, Bun test e muitas outras), o setUp monta a fixture antes de cada teste e o tearDown a limpa depois de cada teste. Como cada teste começa num palco novo e não deixa nada para trás, os testes podem rodar em qualquer ordem, ou um sozinho.

Um executor de testes (test runner) também deve tratar um teste que falhou como um resultado normal. Ele captura o erro de cada teste, anota e segue para o próximo, de modo que uma única execução mostra todas as falhas de uma vez. Um executor que parasse no primeiro erro esconderia os outros.

Estado compartilhado quebra o isolamento

const cart: string[] = [];          // compartilhado por todos os testes

test("adds an item", () => {
	cart.push("book");
	expect(cart.length).toBe(1);
});
test("adds a second item", () => {
	cart.push("pen");
	expect(cart.length).toBe(2);   // só passa se o primeiro teste rodou antes
});
Uma dependência de ordem escondida

Rode o arquivo inteiro e fica verde. Rode só o segundo teste num processo novo e ele falha, porque a lista começa vazia, então depois de um push ela tem 1 item e não 2. O sinal clássico de estado compartilhado é um teste que passa na suíte completa mas falha sozinho ou em outra ordem.

O que torna uma unidade fácil de testar

  • Função pura: para os mesmos argumentos devolve sempre o mesmo valor e não muda nada fora dela. Não precisa de relógio, números aleatórios, arquivos nem rede, então você só passa argumentos e confere o valor devolvido. Coloque a lógica em funções puras e deixe os efeitos colaterais numa camada fina em volta.
  • [[Injeção de dependência|entregar a um trecho de código o que ele precisa vindo de fora, em vez de deixá-lo criar sozinho]]: se registerUser faz new SmtpMailer() lá dentro, o teste não consegue impedir o envio de e-mail de verdade. Se ela recebe um mailer como parâmetro ou no construtor, o teste entrega um mailer de mentira.
  • Testar pela interface pública: confira o que a classe promete (valores devolvidos, estado visível). Um teste que lê um campo privado quebra quando o código é reescrito por dentro, mesmo que nada tenha mudado para os usuários.
  • Objetos com estado: o efeito de um método depende dos métodos chamados antes dele. Testar uma classe Connection com um teste por método não basta: tente também as sequências, inclusive as inválidas, como send depois de close. Todas as linhas podem estar cobertas e a sequência perigosa nunca rodar.
↑ Voltar ao sumário

Dublês de teste: ajudantes de mentira

Um filme usa um dublê nas cenas perigosas. Em software, um dublê de teste é um objeto que ocupa o lugar de um ajudante de verdade (um banco de dados, um servidor de e-mail, um relógio) durante um teste. Fazemos isso porque o ajudante real pode ser lento, custar dinheiro ou ser impossível de controlar.

As palavras abaixo vêm de Gerard Meszaros e são usadas por Martin Fowler. São cinco tipos.

Os cinco tipos de dublê de teste
TipoO que fazExemplo
DummySó preenche um parâmetro que a assinatura exige. Nunca é realmente usado.Um logger passado ao construtor num teste que não registra nada
StubDá respostas prontas às chamadas que recebe. Controla as entradas indiretas do código testado.Um serviço de cotação que sempre responde 5,2
SpyUm stub que também anota como foi chamado. O teste olha as anotações depois de agir.Um mailer escrito à mão que guarda uma lista sent
MockRecebe antes do teste agir quais chamadas esperar, e verifica sozinho.Um objeto de biblioteca com 'send deve ser chamado uma vez com este endereço'
FakeUma versão real e funcional com um atalho que a torna imprópria para produção.Um repositório que guarda os registros num Map em memória

Verificação de estado e verificação de comportamento

  • Verificação de estado: o teste usa a unidade e depois olha o resultado, o valor devolvido ou o estado final dos objetos.
  • Verificação de comportamento: o teste olha as chamadas feitas aos ajudantes. É necessária quando o efeito é a própria chamada, como enviar um e-mail.
  • O segundo estilo amarra o teste a como o código foi escrito. Um teste que só conta chamadas pode passar quando a resposta está errada e falhar depois de uma refatoração inofensiva. Quando existe um valor devolvido ou um estado final, a asserção principal deve ser sobre ele.

Um fake costuma dar testes mais curtos e mais fortes que um mock quando o ajudante tem memória. Se um serviço de cadastro salva um usuário e depois o busca e o atualiza, um repositório fake deixa o teste apenas olhar o usuário guardado no final. Um mock precisaria de uma expectativa para cada chamada, o que descreve a conversa, não o resultado.

Usos que vale conhecer

  • Situações difíceis de provocar: disco cheio, um serviço que responde 503, um timeout. Um dublê configurado para falhar causa o erro sempre que você pedir, e assim você confere como o código reage. O dublê substitui o ajudante (o cliente HTTP), nunca a unidade que está sendo testada.
  • O tempo como dependência: código que lê o relógio do sistema tem uma entrada escondida. Injete um FakeClock que começa num instante fixo e só anda quando o teste chama advance. Assim 'o token expira depois de 15 minutos' é testado em milissegundos, exatamente na borda, e nunca falha por sorte.
  • Self Shunt: o nome dado por Kent Beck ao caso em que a própria classe de teste faz o papel do dublê.
↑ Voltar ao sumário

Desenvolvimento guiado por testes: vermelho, verde, refatorar

A maioria das pessoas escreve o código primeiro e o teste depois. O desenvolvimento guiado por testes (TDD, de test-driven development), criado por Kent Beck, inverte isso: você escreve o teste primeiro. Parece estranho, mas é como desenhar a linha de chegada antes de correr a prova.

    +--------+   fazer passar, rápido    +--------+
    |VERMELHO| ------------------------> | VERDE  |
    | teste  |                           | todos  |
    | novo   |                           | testes |
    | falha  | <------------------------ | passam |
    +--------+    próximo teste pequeno   +---+----+
         ^                                   |
         |   limpar, mesmo comportamento     v
         +------------------------------ REFATORAR
O ciclo do TDD
  1. Vermelho (red): escreva um teste pequeno que falha.
  2. Verde (green): faça-o passar do jeito mais rápido que puder.
  3. Refatorar (refactor): melhore a estrutura e remova duplicação sem mudar o comportamento, com todos os testes ainda passando.

O TDD tem duas regras: escreva código novo só quando um teste automatizado falhou e elimine a duplicação. Se uma ideia surge no meio do trabalho (por exemplo 'este método deveria rejeitar números negativos'), ela primeiro vira um teste, ou uma linha na sua lista de tarefas. A lista de tarefas é a memória de curto prazo do TDD: você escreve os casos que conhece, acrescenta os que aparecem no caminho e os risca quando passam.

Por que ver o teste falhar primeiro?

O passo vermelho testa o próprio teste. Se um teste novo já passa antes de o código existir, ou falha por um motivo bobo como erro de digitação ou import errado, ele não está verificando o que você queria. Só depois de vê-lo falhar pelo motivo certo a barra verde conta como prova.

Três caminhos até o verde

EstratégiaO que você fazQuando serve
Fake it (fingir)Devolve uma constante que faz o teste passar e depois troca a constante por variáveis até aparecer o código realVocê está inseguro e quer uma barra verde já
TriangulaçãoEspera um segundo exemplo antes de generalizar o códigoVocê ainda não conhece a abstração certa. É a mais cuidadosa
Implementação óbviaDigita o código real diretoA solução é clara. É a mais rápida

Os passos podem ser grandes ou pequenos. Quando tudo vai bem, use passos grandes com implementação óbvia. Se aparece um vermelho inesperado, dê passos menores (fake it, triangulação), pois um passo pequeno limita o lugar do erro a poucas linhas.

Lendo o histórico de uma sessão

Cada passo deixa uma assinatura na suíte. Vermelho: um teste novo é adicionado e a suíte vai de verde a vermelho. Verde: o código de produção muda e a suíte vai de vermelho a verde. Refatorar: o código de produção muda, a suíte está verde antes e depois e nenhum teste novo aparece. Nunca misture uma refatoração com uma mudança de comportamento no mesmo passo: os testes que não mudaram são a prova de que o comportamento foi mantido, e se você também altera os valores esperados, perde essa prova.

O exemplo do dinheiro

No livro de Beck, Dollar e Franc são unidos numa só classe Money. Money é um objeto de valor (value object): é definido pelos seus dados, não pela identidade, então dois objetos com os mesmos valores são iguais. Quando as classes são unidas, a moeda vira um campo, e o equals precisa compará-la também.

equals(other: Money): boolean {
	return this.amount === other.amount;   // esquece a moeda
}
// 5 USD é igual a 5 CHF? Um teste que pergunta isso falha e força a correção:
// return this.amount === other.amount && this.currency === other.currency;
Um defeito que um teste novo revelaria

Benefícios e limites

Benefícios apontados para o TDD: cobertura como subproduto, uma suíte de regressão que cresce junto com o programa, depuração mais fácil (o bug está nas poucas linhas recém-escritas) e testes que funcionam como documentação executável. O limite: o TDD produz principalmente testes unitários escritos pelo desenvolvedor, que confere o código contra o que ele entendeu. Formatos incompatíveis entre serviços, lentidão sob carga e a pergunta 'o usuário precisa disso?' pedem outros tipos de teste.

Assista ao vídeo: TDD, Where Did It All Go Wrong (Ian Cooper)↑ Voltar ao sumário

Testes de integração: peças trabalhando juntas

Duas peças de quebra-cabeça podem ser perfeitas e mesmo assim não encaixar. Um teste de integração executa dois ou mais módulos juntos, pela ligação real entre eles. O alvo são os bugs que só existem na fronteira: parâmetros trocados, formatos diferentes, crenças erradas sobre o outro lado, configuração errada. Testes unitários trocam os vizinhos por dublês e por isso nunca atravessam essa fronteira.

Erros de interface

Sommerville separa os erros de uma interface em três classes.

ClasseSignificadoExemplo
Mau uso (misuse)A chamada é feita de modo errado: parâmetros trocados, faltando ou do tipo erradotransfer(to, from, amount) em vez de transfer(from, to, amount)
Mal-entendido (misunderstanding)Quem chama supõe um comportamento que a parte chamada não ofereceSupor que a lista volta ordenada
Temporização (timing)Os dois lados trabalham em velocidades diferentes e um lê dado velhoUm leitor vê dados compartilhados antes de o escritor terminar

Como juntar as peças

  • Big-bang: juntar todos os módulos de uma vez e testar o conjunto no final. Uma falha pode vir de qualquer interface, então é muito difícil de localizar.
  • Incremental: acrescentar um módulo por vez e rodar os testes de novo, de modo que uma falha nova aponta para o que acabou de entrar.

Enquanto algumas peças ainda não existem, escrevemos código de apoio. Um driver chama o módulo testado no lugar de quem estaria acima dele. Um stub responde no lugar de quem estaria abaixo. A integração bottom-up (de baixo para cima) começa com as camadas de baixo reais e usa drivers. A top-down (de cima para baixo) começa com as camadas de cima reais e usa stubs. Imagine três camadas, UI sobre Service sobre Repository. Se a equipe constrói de baixo para cima e a UI ainda não existe, ela escreve um driver que chama o Service.

  [ driver ]  -->  [ Service ]  -->  [ Repository ]
   código de teste     real              real
   (faz o papel da UI que ainda não foi escrita)
Bottom-up com um driver

Banco de dados real ou fake?

Um fake em memória é rápido, mas só sabe o que programamos nele. Um banco de dados tem regras próprias: chaves únicas, chaves estrangeiras, tipos, transações, o SQL de verdade. Um repositório que passa com um Map pode quebrar em produção com PostgreSQL quando dois usuários têm o mesmo e-mail. Por isso uma suíte saudável tem alguns testes de integração contra o banco real, ou ao menos o mesmo motor.

Um banco real é compartilhado, então cada teste precisa começar do mesmo ponto. Um truque comum: abrir uma transação antes de cada teste e desfazê-la (rollback) depois. Tudo o que o teste escreveu desaparece, sem o custo de reconstruir o banco, e os testes não dependem da ordem.

beforeEach(() => db.run("BEGIN"));
afterEach(() => db.run("ROLLBACK"));  // esquece o que o teste escreveu

// o teste 1 insere um pedido e vê 1 linha
// o teste 2 roda depois e volta a ver 0 linhas
Cada teste começa com a tabela vazia

Um teste de integração também pode enviar uma requisição ao handler HTTP dentro do mesmo processo e depois ler a linha num SQLite real. Ele confere a costura inteira: o corpo é lido, convertido, repassado, transformado em SQL e aceito pela tabela. Cada camada pode passar nos seus testes unitários e o conjunto ainda falhar por causa de um nome de coluna diferente.

Testes de contrato

Suponha que o serviço de pedidos seja testado contra um stub do serviço de pagamentos que responde { "total": 30 }, e a equipe de pagamentos renomeie o campo para amount. Os testes das duas equipes passam e a produção quebra. Um teste de contrato resolve isso: o que o consumidor espera vira um arquivo, e esse arquivo é executado contra o provedor real no pipeline do próprio provedor. Você tem a verificação da interface sem subir todos os serviços juntos.

↑ Voltar ao sumário

Testes de ponta a ponta com Playwright

Um teste de ponta a ponta (e2e, de end-to-end) usa o sistema do jeito que uma pessoa usa: pela tela, com todas as camadas reais ligadas. É o que dá mais confiança de que tudo funciona junto. Em troca, é o mais lento, o mais caro de manter e o menos preciso sobre onde está o bug. Por isso fica no topo da pirâmide, em poucas unidades.

O Playwright é uma ferramenta que controla um navegador de verdade a partir de um teste: abre páginas, clica em botões, digita texto e lê o que está na tela.

Auto-waiting (espera automática)

Páginas web não são instantâneas. Um botão pode ficar desabilitado enquanto os dados carregam. O Playwright espera sozinho até o elemento estar pronto antes de agir, então o teste não precisa de pausas fixas como waitForTimeout(2000) (uma pausa ou é longa demais e desperdiça tempo, ou é curta demais num computador lento). Se o elemento nunca fica pronto, a ação falha com um erro de timeout.

Locators: encontre as coisas como o usuário as vê

LocatorEstiloResiste a uma reformulação visual?
getByRole("button", { name: "Checkout" })O que o elemento é para o usuárioSim
getByLabel, getByTextO que o usuário lêSim
CSS ou XPath longo como div > div:nth-child(3) .btn-xComo o HTML por acaso está montado hojeNão, quebra em refatorações que não mudam nada para o usuário

Asserções web-first e o await esquecido

Uma asserção web-first como await expect(locator).toBeVisible() recebe o locator e repete a verificação até passar ou o tempo acabar. Compare com expect(await locator.isVisible()).toBe(true): ela recebe um verdadeiro ou falso já pronto, uma foto de um instante. Se a tela ainda não atualizou, a foto diz falso e o teste falha por sorte.

await page.getByRole("button", { name: "Add to cart" }).click();
// ERRADO: sem await, a promessa fica solta e o teste termina antes da conferência
expect(page.getByTestId("cart-count")).toHaveText("1");
// CERTO:
await expect(page.getByTestId("cart-count")).toHaveText("1");
Duas armadilhas num só teste

As asserções do Playwright são assíncronas e devolvem uma promise. Sem await, o teste corre até o fim sem esperar o resultado, e pode ficar verde mesmo com o contador mostrando 0. Um teste que não consegue falhar é pior do que nenhum teste. Uma regra de lint para promises soltas pega esse erro.

Isolamento por contexto de navegador

Cada teste ganha o seu próprio contexto de navegador (browser context), que funciona como um perfil novo e vazio: cookies, localStorage e cache próprios. Criar um contexto é barato. Então, se o primeiro teste faz login, o segundo teste, que usa a fixture page, abre o site como um desconhecido que não está logado. Os testes não dependem uns dos outros nem da ordem.

Controlando a rede

O page.route intercepta as requisições feitas pela página e deixa o teste decidir: responder com dados inventados, abortar ou deixar passar. Trocar um serviço de terceiros por uma resposta fixa torna o teste estável e independente da internet. O que deixa de ser verificado é a integração real com esse serviço, que pede outro tipo de teste.

↑ Voltar ao sumário

Testes de fumaça e de regressão

Quando você liga uma máquina nova na tomada, primeiro confere se não sai fumaça. Você ainda não testa todas as funções. Um teste de fumaça (smoke test) é igual: um pequeno conjunto de verificações rápidas e rasas dos caminhos críticos. A aplicação inicia? Responde? As funções essenciais funcionam? Ele roda logo depois de um build ou deploy e age como um porteiro: se falha, a versão é recusada na hora e ninguém perde tempo com as suítes longas.

Uma regressão é a perda de um comportamento que antes estava correto, causada por uma mudança em algum lugar do código. Teste de regressão é rodar de novo os testes existentes depois de cada mudança para ver que nada que funcionava parou de funcionar. Com testes automatizados isso é barato e pode rodar a cada commit.

Fumaça contra regressão
AspectoSuíte de fumaçaSuíte de regressão completa
ProfundidadeRasa, muitas áreas de leveFunda, em detalhe
TempoSegundos (por exemplo 30 s)Longo (por exemplo 40 min)
PerguntaVale a pena testar mais esta versão?Tudo o que funcionava ainda funciona?
QuandoLogo depois do build ou deploy, primeiroDepois que o teste de fumaça passa e antes de um release

As suítes são ordenadas da mais barata para a mais cara, para que a falha aconteça o mais cedo possível. O teste de sanidade (sanity test) é um termo que as equipes usam de jeitos diferentes, mas em geral significa uma verificação estreita da área que acabou de ser mudada.

O que entra na suíte de fumaça é escolhido pela criticidade, não pela profundidade: os caminhos sem os quais o sistema não serve para nada (abrir o site, entrar, a operação principal). Detalhes de regras, casos-limite e bugs antigos ficam nas suítes unitárias e de regressão. Se a suíte de fumaça crescer demais, perde a rapidez que a justifica.

Todo bug vira um teste

Um usuário relata que um pedido com cupom de 100% mostra total negativo. O que o desenvolvedor deve fazer primeiro? Escrever um teste que reproduz o bug e falha. Só depois corrigir o código e ver o teste ficar verde. O primeiro vermelho prova que o teste realmente enxerga o problema, e o teste fica para sempre protegendo contra a volta desse bug.

Quando a suíte de regressão é lenta demais

Se a suíte completa leva 3 horas, uma equipe pode rodar a cada commit apenas os testes dos arquivos que importam os módulos alterados. Isso dá feedback rápido, mas é uma aposta: dependências indiretas podem fazer uma mudança quebrar algo que parece longe. A proteção usual é continuar rodando a suíte completa em intervalos regulares e antes de cada release.

↑ Voltar ao sumário

Cobertura e teste de mutação

Como saber se os seus testes são bons? Uma primeira ideia é contar quanto do código eles executam. Isso se chama cobertura (coverage).

Tipo de coberturaO que conta
De linhas (ou de instruções)Quantas linhas foram executadas por pelo menos um teste
De ramos (branch)Se cada decisão (if) foi tomada dos dois jeitos: verdadeira e falsa
export function shippingFee(total: number, isMember: boolean): number {
	let fee = 10;
	if (isMember) fee = 5;
	if (total >= 100) fee = 0;
	return fee;
}

test("big member order is free", () => {
	expect(shippingFee(150, true)).toBe(0);
});
Todas as linhas rodam, mas só metade dos ramos

Este único teste passa por todas as linhas, então a cobertura de linhas é 100%. Mas as duas condições dos if só são verdadeiras. Cada if tem dois ramos, então são 4 ramos e o teste percorreu 2: a cobertura de ramos é 50%. 100% das linhas não significa 100% dos ramos.

Teste de mutação: testar os testes

O teste de mutação faz a pergunta melhor: os testes notariam se esta linha estivesse errada? Uma ferramenta faz cópias do programa, cada uma com um pequeno defeito plantado, chamada de mutante. A ferramenta troca coisas como + por -, < por <=, uma constante por outra, ou remove uma instrução. Depois roda a suíte em cada mutante.

programa original --- muda uma coisinha ---> mutante
                                                  |
                                       roda a suíte de testes
                                                  |
                 +--------------------------------+--------------+
                 |                                               |
      algum teste falha: MORTO (killed)         todos passam: SOBREVIVEU
      a suíte percebeu o bug                    a suíte aceita o bug
O que acontece com cada mutante

Um mutante que sobreviveu é uma pergunta que os testes não fizeram: mostra onde falta uma asserção ou um teste.

O escore de mutação

O escore de mutação é os mutantes mortos divididos pelos mutantes que poderiam ser mortos. Um mutante equivalente tem texto diferente mas se comporta exatamente como o original para qualquer entrada, então nenhum teste consegue matá-lo. Pela convenção usual, os mutantes equivalentes saem do denominador. Exemplo: uma ferramenta gera 30 mutantes e a suíte mata 24. Dos 6 sobreviventes, uma pessoa decide que 2 são equivalentes. O escore é 24 / (30 - 2) = 24 / 28, cerca de 85,7%. Muitas ferramentas não identificam equivalentes e só informam mortos sobre o total, então diga sempre qual convenção está em uso. Decidir se um mutante é equivalente é impossível no caso geral, por isso uma pessoa costuma revisar os sobreviventes.

if (weightKg > 2) {                    // mutante: weightKg >= 2
	cents += (weightKg - 2) * 150;     // em exatamente 2 kg soma (2 - 2) * 150 = 0
}
Um mutante equivalente

Matando um mutante de fronteira

Pegue return age >= 18 e o mutante return age > 18. Eles só diferem para a entrada 18. Um teste só mata o mutante se usar esse valor exato e conferir o resultado: isAdult(18) deve ser verdadeiro. Testes com idades 10 e 30 deixam o mutante viver. Esse é o motivo prático para testar os valores-limite.

Mesma cobertura de linhas, escore de mutação muito diferente (mini-projeto mutation-testing)
suíte fraca: cobertura de linhas
100 %
suíte forte: cobertura de linhas
100 %
suíte fraca: escore de mutação
21.1 %
suíte forte: escore de mutação
94.7 %

O custo do teste de mutação cresce com o número de mutantes vezes o tempo da suíte. As ferramentas o reduzem rodando, para cada mutante, só os testes que cobrem a linha alterada, parando no primeiro teste que falha, mutando apenas o código alterado desde a última execução e amostrando os mutantes.

↑ Voltar ao sumário

Testes baseados em propriedades

Um teste comum é um exemplo: 'ordenar [3,1,2] e obter [1,2,3]'. Um teste baseado em propriedades enuncia uma regra geral, como 'ordenar nunca muda o tamanho da lista', e um gerador inventa centenas de entradas aleatórias para tentar quebrar a regra. Ele acha casos em que você não pensou. Mas só confere as entradas que foram geradas, então não é uma prova.

Formas de propriedade

FormaRegraExemplo
Ida e volta (round trip)Ir e voltar leva ao começodecode(encode(x)) é igual a x
InvarianteAlgo que a operação mantémOrdenar mantém o mesmo tamanho e os mesmos itens
IdempotênciaFazer duas vezes é o mesmo que umanormalize(normalize(s)) é igual a normalize(s)
ComutatividadeA ordem não importaadd(a, b) é igual a add(b, a)
OráculoComparar com uma versão simples e confiávelUma ordenação rápida contra uma lenta, mas obviamente certa

Shrinking (encolhimento)

Uma entrada aleatória que falha costuma ser grande e cheia de detalhes inúteis, como uma lista de 37 números. O shrinking procura sozinho uma entrada mínima que ainda falha, por exemplo [0, -1]. Um exemplo pequeno deixa a causa do bug fácil de ver.

A semente (seed)

A aleatoriedade vem de um gerador pseudoaleatório, cuja sequência inteira é decidida por um único número, a semente (seed). Quando um teste de propriedade falha no CI, a ferramenta imprime algo como seed: 48213. Rode o teste de novo com a mesma semente e você obtém exatamente as mesmas entradas. Sem isso, um teste aleatório seria um teste flaky.

Uma propriedade que não prova nada

function average(a: number, b: number) { return Math.floor((a + b) / 2); }

for (let i = 0; i < 1000; i++) {
	const a = randomInt(-100, 100), b = randomInt(-100, 100);
	expect(average(a, b)).toBe(Math.floor((a + b) / 2));  // a mesma fórmula
}
Uma tautologia

O lado esperado repete a fórmula do código, então o teste passa por construção e não detecta nada. Uma boa propriedade enuncia uma relação exigida pela especificação e independente do código, como 'a média fica entre o menor e o maior dos dois números'.

Propriedades devem substituir os exemplos? Não. Propriedades exploram muitas entradas, mas as boas são difíceis de achar e são regras gerais, mais fracas que um valor exato. Exemplos documentam o comportamento com números reais e garantem que casos-limite conhecidos sempre rodem. Funcionam bem juntos.

↑ Voltar ao sumário

Testes intermitentes (flaky)

Um teste flaky (intermitente) passa e falha no mesmíssimo código. Nada mudou entre a execução verde e a vermelha. Isso acontece quando o resultado depende de algo que o teste não controla: o relógio, a ordem dos testes, estado compartilhado, a rede, concorrência ou números aleatórios. O sinal típico é uma falha que some quando você roda de novo.

Esse é o dano real. O valor de uma suíte é a confiança: vermelho significa bug, verde significa que é seguro seguir. Testes flaky quebram isso nos dois sentidos, geram alarmes falsos e ensinam a equipe a simplesmente rodar de novo em vez de investigar. Depois disso um bug de verdade pode passar.

Chances pequenas se somam

Se cada um de n testes independentes falha por sorte com probabilidade p, a suíte passa inteira com probabilidade (1 - p) elevado a n. Logo, mostra pelo menos uma falha com probabilidade 1 - (1 - p)^n.

Execuções vermelhas sem bug real, quando cada teste flaky falha 2% das vezes
1 teste flaky
2 %
10 testes flaky
18 %
50 testes flaky
64 %

Com 50 testes de 2% cada, 0,98^50 é cerca de 0,364, então 1 - 0,364 é cerca de 0,636. A suíte fica vermelha quase duas execuções em cada três. O mesmo acontece com uma suíte de 300 testes de ponta a ponta que falham por sorte 0,5% das vezes cada um: 0,995^300 é cerca de 0,22, então perto de 78% das execuções mostram uma falha com o código correto.

As causas usuais e suas correções

CausaExemploCorreção
TempoO teste lê o relógio duas vezes e espera o mesmo milissegundoInjetar um relógio falso com um instante fixo
Fuso horárioO código data em hora local, o teste em UTC, e discordam perto da meia-noiteRelógio falso, e comparar datas no mesmo fuso. Testar de propósito um instante dentro da janela de risco
sleep fixoO teste dorme 200 ms e confere. Numa máquina lenta o trabalho não terminouEsperar a condição: aguardar a promise ou consultar com um timeout
Ordem dos resultadosTrabalhos concorrentes terminam numa ordem diferente a cada vezGarantir a ordem no código, ou comparar os resultados ordenados
Estado compartilhadoUm teste usa um cache que só outro teste preencheCada teste monta e restaura a sua própria fixture
Rede realO serviço tem um momento ruimUsar um stub e testar o serviço real num teste de contrato separado

Embaralhar a ordem dos testes e imprimir a semente é um jeito de achar e repetir uma dependência de ordem, não de curá-la. Se o B só passa quando roda depois do A, a cura é o isolamento: o B cria os próprios dados e limpa depois de si, para passar sozinho e em qualquer posição.

O que não é correção

  • Tentar de novo até ficar verde: esconde o sintoma e mantém a causa.
  • Um sleep maior: torna a corrida menos provável e a suíte mais lenta.
  • Apagar ou pular o teste sem entendê-lo: às vezes o produto tem uma corrida de verdade.
  • Fixar a ordem dos testes para o estado compartilhado continuar funcionando: a dependência continua lá e impede rodar em paralelo.
↑ Voltar ao sumário

Projeto de testes: classes de equivalência e valores-limite

Você não consegue testar todas as entradas possíveis. Só um campo numérico já tem bilhões. Projetar testes é a arte de escolher poucas entradas que dizem muito.

Particionamento em classes de equivalência

Divida as entradas possíveis em grupos, chamados de classes de equivalência, em que o programa deve tratar todos os membros do mesmo jeito. Depois escolha pelo menos um representante de cada classe. As classes vêm da especificação, não do código, e incluem classes de entradas inválidas.

Contando as classes: uma classe para cada faixa que a especificação trata de modo diferente. Para uma entrada numérica com faixas separadas, são uma classe por faixa válida, mais uma classe inválida abaixo do menor valor válido e outra acima do maior. Se uma tabela de preços tem 3 faixas de peso, mais erros para leve demais e pesado demais, são 3 + 2 = 5 classes e pelo menos 5 casos de teste.

Análise de valores-limite

Programadores erram bem nas bordas: < no lugar de <=, um laço que para um passo antes. Um valor do meio de uma classe passa pelo código defeituoso sem perceber. Por isso, além do valor típico, teste a borda e o vizinho do outro lado.

  rejeitado |     faixa aceita        | rejeitado
  ... -1  0 | 1  2  3 ... 18 19  20 | 21  22 ...
           ^  ^                  ^   ^
          a-1 a                  b  b+1
  valores de teste: 0, 1, 20, 21
Análise de limites com dois valores para a faixa fechada [1, 20]

A convenção deste quiz: para uma faixa fechada de números inteiros [a, b], o método de dois valores testa a-1, a, b e b+1. Imagine que o código diz grade >= 0 && grade < 10 quando a especificação diz que as notas de 0 a 10 são válidas. Só o valor 10 dá uma resposta diferente da especificação (falso em vez de verdadeiro). Todos os outros valores de teste ficam do lado certo, então só o valor na fronteira mostra o bug.

Várias entradas: uma inválida de cada vez

Quando uma função tem várias entradas, as classes válidas podem ser combinadas num só teste. Mas cada classe inválida deve ficar sozinha, com as outras entradas válidas. Um programa normalmente para na primeira verificação que falha. Se um teste dá uma idade inválida e uma renda inválida juntas, a verificação da renda nunca é alcançada, e você nunca descobre que ela existe e funciona.

Diretrizes para sequências

A experiência diz onde os programas costumam quebrar. Para listas e outras sequências: teste a sequência vazia, uma sequência com um elemento e tamanhos diferentes em testes diferentes. Faça também o elemento interessante aparecer primeiro, no meio e por último. Para uma função que devolve o maior número, boas entradas incluem [], [7], [9, 2, 3], [2, 9, 3] e [2, 3, 9].

Tabelas de decisão

Quando o resultado depende de várias condições de sim ou não, uma tabela de decisão lista as combinações e a ação esperada para cada uma. Com n condições independentes de sim ou não, a tabela completa tem 2^n colunas, e cada coluna vira pelo menos um teste. Se uma condição decide sozinha, as colunas que diferem só nas outras condições podem ser unidas, escrevendo 'tanto faz' (don't care) para elas.

Quatro condições, 16 colunas. Quando a primeira condição é falsa a resposta é sempre 'recusar'
ColunasQuantas
Primeira condição falsa: unidas numa coluna com 'tanto faz'1
Primeira condição verdadeira: as outras 3 condições dão 2 x 2 x 2 combinações8
Total depois de unir9
↑ Voltar ao sumário

Testes em CI e estratégia de testes

Integração contínua (CI) significa que todos compartilham o trabalho com a linha principal com frequência, e cada compartilhamento é verificado por um build e testes automáticos. Os testes rodam a cada push, então o feedback chega rápido e aponta para a mudança que acabou de ser feita. Quando o build quebra, consertá-lo vem antes de trabalho novo, porque todos dependem da linha principal.

Ordene as etapas pelo custo

Um pipeline arranja suas verificações da mais barata para a mais cara. Com fail-fast, a execução para na primeira etapa que falha, então um erro que uma verificação de 50 ms encontra não espera atrás de uma etapa de minutos.

  push --> [ lint + tipos + unit ] --> [ integração ] --> [ e2e ] --> merge
              segundos                  dezenas de seg.     minutos
               |                          |                  |
               falha: para aqui           falha: para aqui   falha: para
Um pipeline fail-fast

Um teste flaky bloqueia todo mundo: ponha em quarentena

Um pipeline só é útil se vermelho significa defeito. Suponha que um teste falha em 1 de 10 execuções e bloqueia merges, e a equipe não consegue consertá-lo hoje. Os maus caminhos escondem o problema (tentativas silenciosas, apagar o teste, enfraquecer suas asserções) ou mantêm o ruído (rodar de novo à mão). O bom caminho é uma quarentena explícita: o teste sai do caminho bloqueante, a dívida é anotada com um responsável e uma data, e ele continua rodando em algum lugar para não ser esquecido.

Estratégia baseada em risco

Você não consegue testar tudo, então distribua o esforço pelo risco: probabilidade de haver um defeito vezes o seu impacto. A probabilidade é maior em código novo ou muito alterado, lógica complexa e partes com histórico de falhas. O impacto é maior quando envolve dinheiro, segurança ou muitos usuários. A cobertura é só uma das entradas.

Faltam dois dias para entregar uma loja online
MóduloMudou muito?Impacto de um defeitoCobertura de linhas
Página 'Sobre nós'NãoTexto errado numa página estática0%
Exportação de relatórioNão2 usuários internos, uma vez por mês40%
Cálculo de preço e impostoSim, muitoCliente cobrado com valor errado85%
Cores do tema do painelUm poucoAparência de uma página interna10%
Cadastro na newsletterNãoUm e-mail de marketing não é enviado60%

O módulo de preço e imposto tem o maior risco: mudou muito e um erro custa dinheiro de verdade. Ele deve receber o esforço mesmo que a sua cobertura já seja a mais alta, porque cobertura alta não torna um módulo seguro.

↑ Voltar ao sumário

Juntando tudo

Cada técnica responde a uma pergunta diferente. Um projeto saudável usa várias delas, porque cada uma é cega para algo que outra enxerga.

PerguntaFerramenta
Esta peça pequena funciona sozinha?Teste unitário, com dublês para ajudantes lentos ou incontroláveis
As peças se encaixam?Teste de integração, teste de contrato
Um usuário consegue mesmo fazer as jornadas principais?Alguns testes de ponta a ponta com Playwright
Vale a pena testar esta versão?Teste de fumaça
Um bug antigo voltou?Teste de regressão, um para cada bug encontrado
Meus testes executam o código?Cobertura de linhas e de ramos
Meus testes perceberiam um bug?Teste de mutação
Esqueci alguma entrada estranha?Testes baseados em propriedades, classes de equivalência e valores-limite
Meus testes dizem a verdade?Eliminar flakiness: relógios falsos, isolamento, stubs
Alguém está quebrando o código compartilhado?CI com etapas ordenadas

Erros comuns

  • Colocar toda verificação em testes de ponta a ponta (o cone de sorvete).
  • Contar chamadas a dublês em vez de conferir o resultado.
  • Confiar num número de cobertura sem perguntar se os testes conferem alguma coisa.
  • Corrigir um bug sem escrever o teste que o reproduz.
  • Deixar os testes compartilharem estado ou dependerem do relógio, da ordem ou da rede.
  • Refatorar e mudar o comportamento no mesmo passo.
  • Esquecer o await numa asserção assíncrona.
↑ Voltar ao sumário