Escrever Testes vs Testar
Um time pode ter 90% de cobertura, uma suíte enorme e ainda assim entregar bug em produção. Não é contradição: escrever testes e testar são atividades diferentes, e a maioria das equipes pratica só a primeira.
A nota segue as ideias da palestra “Escrever testes vs testar”, de Maurício Aniche (professor na TU Delft e autor de livros sobre testes), e junta com o que já aparece no restante do lab.
Duas atividades diferentes
Seção intitulada “Duas atividades diferentes”Escrever testes é o que acontece no TDD: você escreve um teste, faz passar, refatora. O foco é o ritmo de trabalho, o design do código e a confiança para mexer nele depois. Quem escreve teste desse jeito costuma testar os casos que veio à cabeça na hora, o que é natural e, sozinho, insuficiente.
Testar é procurar bugs de forma sistemática. A pergunta deixa de ser “o código faz o que eu imaginei?” e vira “onde esse código pode estar errado?”. Para responder, existem técnicas com nome e método, várias delas pouco ensinadas em quem aprende só a escrever teste.
Uma comparação ajuda: TDD é um bom jeito de andar de bicicleta com capacete e freio. Testar é olhar o mapa e conferir se você não vai cair em um buraco que nunca apareceu no caminho de casa.
Uma não substitui a outra:
- só escrever testes deixa passar os casos que ninguém pensou em escrever
- só testar, sem o ritmo do TDD, tende a deixar o código difícil de testar e o design mais frágil
flowchart LR
A["Escrever testes (TDD)"] --> B["Design mais simples e confiança para refatorar"]
C["Testar (técnicas sistemáticas)"] --> D["Mais bugs encontrados antes da produção"]
B --> E["Código de qualidade"]
D --> E
As seções abaixo passam pelas técnicas que a palestra apresenta como o outro lado da moeda.
Análise do domínio e testes de limites
Seção intitulada “Análise do domínio e testes de limites”O ponto de partida é olhar para o domínio: quais valores de entrada existem e como o programa reage a cada faixa deles. Bugs gostam de morar nas fronteiras, porque é ali que um > acaba escrito no lugar de >=.
Exemplo: um site libera conteúdo para maiores de 18 anos.
export function podeAcessar(idade: number): boolean { return idade >= 18;}Se o limite é 18, testar só com 10 e 40 anos dá uma falsa sensação de segurança, porque os dois passam mesmo com o > errado. Os casos que importam estão em volta do limite: um pouco abaixo, exatamente nele e um pouco acima (17, 18 e 19; na palestra, o exemplo é “se x > 10, teste 9, 10 e 11”).
import { expect, it } from "vitest";import { podeAcessar } from "./acesso";
it.each([ [17, false], [18, true], [19, true],])("idade %i: acesso liberado = %s", (idade, esperado) => { expect(podeAcessar(idade)).toBe(esperado);});Duas técnicas de caixa-preta andam juntas aqui: a partição de equivalência (dividir as entradas em grupos que o programa trata igual e testar um representante de cada) e a análise de valor limite. As duas estão explicadas em Terminologias de QA.
Modelagem do sistema
Seção intitulada “Modelagem do sistema”Quando o comportamento é complexo, ajuda criar um modelo simplificado antes de sair escrevendo casos. Pode ser uma tabela de decisão, um fluxograma ou uma máquina de estados. O modelo é uma abstração que deixa à mostra as regras e as combinações que você teria que descobrir aos poucos.
Pegue o status de um pedido:
stateDiagram-v2
[*] --> Criado
Criado --> Pago: pagamento aprovado
Criado --> Cancelado: cancelamento
Pago --> Enviado: despacho
Pago --> Cancelado: estorno
Enviado --> Entregue: entrega confirmada
Cada seta vira pelo menos um teste. E o desenho mostra as perguntas que faltam: dá para cancelar um pedido já Enviado? O que acontece se o pagamento for aprovado duas vezes? Muitos bugs aparecem no papel, antes de virar código.
Cobertura estrutural
Seção intitulada “Cobertura estrutural”Cobertura é a medida de quanto do código os testes executam. A cobertura estrutural olha para dentro do código: linhas, ramos de if, condições.
O uso mais útil não é como meta, e sim como lupa. Um relatório mostra quais trechos nenhum teste tocou. Muitos desses trechos existem porque o programador tratou casos que o requisito nunca mencionou: um else para um valor estranho, um tratamento de erro. São candidatos a bug, porque ninguém decidiu o que deveria acontecer ali.
Alguns cuidados:
- cobertura alta não prova qualidade: um teste sem asserção “cobre” a linha e não verifica nada
- cobertura baixa prova pouca coisa: é um sinal para investigar, não para culpar
- cobertura de ramos (branch coverage) diz mais que a de linhas, porque um
ifsem o casoelsetestado tem a linha coberta e o comportamento não
Usar a cobertura para descobrir o que falta testar dá bom resultado. Transformar em meta obrigatória do time costuma dar o efeito contrário.
Design por contrato
Seção intitulada “Design por contrato”Design por contrato é uma ideia de Bertrand Meyer (a linguagem Eiffel foi criada em torno dela): cada operação de uma classe assume compromissos explícitos.
- Pré-condições: o que precisa ser verdade antes de chamar (quem chama é responsável)
- Pós-condições: o que a operação garante depois de executar (quem implementa é responsável)
- Invariantes: o que precisa ser verdade em qualquer momento de vida do objeto
Exemplo com um saque:
class Conta { constructor(private saldo: number) {}
sacar(valor: number): void { // pré-condições if (valor <= 0) throw new Error("O valor do saque deve ser positivo"); if (valor > this.saldo) throw new Error("Saldo insuficiente");
const saldoAntes = this.saldo; this.saldo -= valor;
// pós-condição: o saldo diminuiu exatamente o valor sacado if (this.saldo !== saldoAntes - valor) throw new Error("Pós-condição violada"); // invariante: o saldo nunca fica negativo if (this.saldo < 0) throw new Error("Invariante violada"); }}Para quem testa, os contratos servem de mapa: cada pré-condição é um caso negativo (o que acontece com valor zero, negativo, maior que o saldo?), e cada pós-condição e invariante é uma verificação que pode ser feita depois de qualquer teste.
Atenção com a confusão de nomes: design por contrato trata das promessas dentro do código, entre quem chama e quem implementa. Teste de contrato trata do acordo entre dois serviços diferentes. São ideias diferentes, e a segunda tem nota própria: Testes de contrato.
Teste de mutação
Seção intitulada “Teste de mutação”Uma pergunta incômoda: se eu estragar o código de propósito, meus testes percebem?
Teste de mutação responde isso. Uma ferramenta introduz pequenos defeitos no código de produção (troca >= por >, + por -, remove uma linha) e roda a suíte para cada versão alterada, chamada de mutante.
- Se algum teste falha, o mutante foi morto. Bom sinal.
- Se todos os testes continuam passando, o mutante sobreviveu. A suíte não notou a mudança.
A proporção de mutantes mortos vira o mutation score. Mutante sobrevivente é uma pista direta: ou falta um teste, ou falta uma asserção. No exemplo da idade, o mutante idade > 18 sobrevive se os testes usarem só 10 e 40 anos, e morre quando o teste com exatamente 18 entra. Testes de limite e teste de mutação se completam.
Para JavaScript e TypeScript, a ferramenta mais conhecida é o Stryker. Vale a ressalva: rodar mutação em tudo é lento, então costuma ser aplicado em partes críticas do código.
Geração automática de testes
Seção intitulada “Geração automática de testes”Outra linha de pesquisa é gerar testes por ferramenta. Duas que aparecem na palestra:
- EvoSuite: gera testes de unidade para código Java, usando algoritmos de busca que tentam alcançar o máximo de cobertura.
- EvoMaster: gera testes de sistema para APIs (REST e outras), chamando os endpoints e analisando as respostas.
Elas ajudam a encontrar coisas como exceções inesperadas e casos de borda que ninguém pensou em escrever. O limite é que a ferramenta sabe o que o código faz, mas não o que ele deveria fazer. Um teste gerado a partir de um comportamento errado aprova o erro. Por isso a saída deve passar por revisão de gente, e a técnica complementa, e não substitui, o trabalho de quem especifica os casos.
Geração de casos de teste com IA generativa
Seção intitulada “Geração de casos de teste com IA generativa”EvoSuite e EvoMaster partem do código: geram testes olhando para o que já foi implementado. Uma linha diferente parte do requisito, antes de qualquer código existir: você descreve o comportamento esperado em linguagem natural e um LLM (ChatGPT, Gemini, Claude, Copilot, entre outros) devolve uma lista de cenários de teste.
O fluxo típico:
flowchart LR A[Requisito ou user story] --> B[LLM] B --> C[Rascunho de casos de teste] C --> D[Revisão de quem testa] D --> E[Casos finalizados]
Pegue um requisito simples: “como usuária, quero redefinir minha senha usando o e-mail cadastrado”. Um LLM bem instruído devolve cenários como:
- e-mail cadastrado e válido
- e-mail não cadastrado
- link de redefinição expirado
- validação da nova senha (tamanho, caracteres exigidos)
- confirmação de entrega do e-mail
- casos de borda e negativos que não foram mencionados no requisito
Repare que isso não é uma técnica nova: são as mesmas partição de equivalência, análise de valor limite e teste positivo/negativo de sempre, só que a pessoa que testa não lista os casos na mão, ela escreve o requisito e revisa o que o modelo devolveu.
Uma parte do jargão merece uma ressalva: nem toda ferramenta que “gera teste a partir de requisito” é um agente de verdade. Um agente, no sentido técnico, decide e executa passos por conta própria (buscar o requisito num sistema, abrir uma tarefa, rodar o teste gerado). Boa parte do que circula por aí é só um prompt bem escrito num LLM comum, o que já ajuda bastante, mas é bom saber a diferença na hora de escolher ferramenta ou entender uma vaga.
Uma variação mais sofisticada usa RAG (retrieval-augmented generation, geração aumentada por recuperação): antes de responder, o sistema busca documentos reais do projeto (requisitos anteriores, casos de teste existentes, documentação) e usa isso como contexto, em vez de depender só do que o modelo aprendeu no treinamento. Um estudo de caso industrial mostrou ganho de qualidade nos cenários gerados justamente por causa desse contexto adicional.
Os riscos são os mesmos de qualquer uso de LLM: o modelo pode “alucinar” um comportamento que o sistema não tem, ignorar uma regra de negócio que não estava escrita no requisito, ou repetir um viés dos dados de treinamento. Por isso o fluxo saudável sempre passa por uma pessoa:
gerado pela IA → revisado por quem testa → refinado → finalizadoO ganho real não é “não escrever mais caso de teste”, é economizar o trabalho repetitivo de listar cenários óbvios, sobrando mais tempo para teste exploratório e para pensar nos casos que exigem conhecimento do negócio, que a IA não tem.
Como combinar as técnicas no dia a dia
Seção intitulada “Como combinar as técnicas no dia a dia”Ninguém precisa usar tudo em toda funcionalidade. Uma rotina realista:
- Escrever o código guiado por testes (TDD), pelo design e pelo ritmo.
- Olhar as entradas e listar as faixas de valores e os limites; acrescentar os casos que faltam.
- Se a regra tem estados ou combinações, desenhar o modelo e derivar casos dele.
- Conferir o relatório de cobertura para achar trechos que nenhum teste tocou.
- Em partes críticas, rodar teste de mutação para ver se a suíte percebe defeitos.
Testar bem é dar mais atenção ao que pode dar errado do que ao que você já sabe que funciona.
Referências
Seção intitulada “Referências”- Escrever testes vs testar - Maurício Aniche, pt-BR
- Keynote - Escrever Testes vs. Testar - Maurício Aniche, pt-BR
- Testes automatizados de software: um guia prático - Maurício Aniche, Casa do Código, pt-BR
- Effective Software Testing - Maurício Aniche, Manning, en
- Documentação do Stryker sobre mutation testing - Stryker, en
- Certified Tester - Testing with Generative AI (CT-GenAI) - ISTQB, en
- Generating Test Scenarios from NL Requirements using Retrieval-Augmented LLMs: An Industrial Study - arXiv, en