Testes que valem o que custam
Teste não existe para provar que o código está certo. Existe para você poder mudá-lo amanhã sem medo. Essa mudança de objetivo reorganiza tudo.
Comece pela pergunta errada e você chega a uma suíte cara e inútil.
A pergunta errada é "quanto do meu código está coberto?". A certa é "eu tenho coragem de refatorar isto?".
Se a resposta for não, a suíte não está fazendo o trabalho, independentemente da porcentagem.
Pirâmide, troféu, e a briga que não importa
A pirâmide clássica: muitos testes unitários na base, alguns de integração no meio, poucos de ponta a ponta no topo. A lógica é custo e velocidade: quanto mais alto, mais lento e mais frágil.
O troféu, do Kent C. Dodds, redistribui: poucos unitários, muitos de integração, poucos de ponta a ponta, e análise estática na base. O argumento é que a maior parte dos bugs reais está na interação entre as peças.
Quem está certo depende do que o seu sistema faz.
Se você tem lógica de negócio densa (cálculo de preço, imposto, regra de elegibilidade), a pirâmide serve: existe muita coisa isolada e determinística que vale a pena travar.
Se o seu sistema é sobretudo cola entre banco, fila e API, o troféu serve melhor: testar cada classe isolada com mock não prova quase nada.
A pergunta que eu prefiro à briga de formato
Se este teste passar, o que exatamente eu posso afirmar sobre o sistema?
Se a resposta for "que este método chama aquele outro", o teste vale pouco.
Se for "que um pedido com cupom expirado é rejeitado", vale muito.
Os atributos de um teste bom
Kent Beck listou, e a lista é prática:
Rápido. Suíte lenta deixa de ser rodada antes do commit, e o retorno vai de segundos para meia hora.
Determinístico. Mesmo código, mesmo resultado, sempre.
Isolado. Não depende de outro teste nem da ordem de execução.
Legível quando falha. A mensagem deve dizer o que quebrou sem você abrir o depurador.
Sensível a bug de verdade e insensível a refatoração. Esta é a mais difícil e a mais importante.
Os dublês, e por que mock é perigoso
Os nomes são específicos e as pessoas trocam:
Stub devolve resposta pronta. Você usa para controlar a entrada.
Mock verifica interação: "esse método foi chamado uma vez com esses argumentos".
Fake é uma implementação de verdade, simplificada: um repositório em memória.
Spy é o real, embrulhado, registrando as chamadas.
Quando você verifica interação, você trava o desenho interno do código.
Aí você refatora (extrai um método, muda a ordem de duas chamadas, troca uma dependência por outra) e vinte testes ficam vermelhos sem que nenhum comportamento tenha mudado.
Isso ensina o time que teste é obstáculo. E ensina certo, porque naquele caso é mesmo.
A regra: teste comportamento observável, não implementação. Entra isso, sai aquilo. Use mock só na borda que você não controla: o gateway de pagamento, o envio de e-mail, o relógio.
Na dúvida entre mock e fake, prefira fake. Um repositório em memória de trinta linhas resolve mais e quebra menos.
- Stubcontrola a entradaDevolve resposta pronta. Não verifica nada, só alimenta o caso.
- Fakea escolha padrãoImplementação de verdade, simplificada. Um repositório em memória de trinta linhas.
- Spyo real, observadoO objeto de verdade, embrulhado, registrando as chamadas.
- Mocktrava o desenhoVerifica interação. Refatorar deixa vinte testes vermelhos sem nenhum comportamento ter mudado.
Os sintomas de teste frágil
Se você reconhecer três destes, o seu problema não é falta de teste, e sim excesso do tipo errado:
→ quebra quando você renomeia um método privado
→ depende da ordem de execução
→ usa mock encadeado em três níveis
→ verifica formatação de string em vez do dado
→ tem mais linhas de preparação do que de verificação
→ ninguém do time consegue explicar o que ele garante
Cobertura: mapa, não meta
Cobertura mede o que foi executado, nunca o que foi verificado. Um teste sem nenhum assert dá cem por cento na linha que executa.
Como meta, ela vira farsa. Lei de Goodhart: quando a medida vira meta, deixa de ser uma boa medida. Com meta de oitenta por cento, o time cobre getter e DTO (barato e inútil) e deixa de fora a regra difícil.
Como usar bem:
→ Como mapa: olhe o que está em zero, principalmente em código crítico.
→ Na diferença: exija cobertura no código novo do PR, não um número global. A curva sobe sozinha, no lugar certo.
→ Cobertura de ramo, não de linha.
E se você quiser a resposta honesta sobre a qualidade da sua suíte, existe um teste melhor que qualquer relatório: teste de mutação. A ferramenta sabota o seu código (troca > por >=, inverte um if, remove uma chamada) e roda a suíte. O que continuar verde não estava sendo verificado.
Você pode fazer a versão manual em dois minutos: inverta uma comparação no cálculo mais crítico e rode. Se passar, o silêncio responde.
Teste instável mata a suíte
Teste que passa e falha com o mesmo código é pior que teste faltando, porque ensina o time a ignorar vermelho.
Com dois por cento de falha aleatória numa suíte de quinhentos testes, quase toda execução tem um vermelho. Em duas semanas, "roda de novo" virou processo.
As causas: sleep fixo (troque por espera com condição e prazo); dependência de ordem; concorrência real; recurso externo, incluindo relógio, aleatoriedade e fuso.
A política: instável vira issue com dono e prazo. Não vira retry automático: o retry esconde exatamente o bug de concorrência que o teste estava, corretamente, encontrando.
Como achar os seus: rode a suíte vinte vezes no mesmo commit, à noite. Três ou quatro testes costumam responder por oitenta por cento dos vermelhos falsos.
TDD, sem religião
O ciclo é vermelho, verde, refatora. O ganho principal não é o teste que sobra: é você ser obrigado a usar a sua própria interface antes de implementá-la.
Onde brilha: regra de negócio com entrada e saída claras; correção de bug.
Onde atrapalha: exploração, quando você ainda não sabe o desenho; integração com sistema de terceiro que você ainda não entende.
E o uso menos controverso e mais rentável, que eu recomendaria a qualquer time, mesmo os que não fazem TDD:
Todo bug corrigido entra com um teste que falha antes do conserto.
Custa quinze minutos, prova que você entendeu a causa, e garante que aquele bug não volta. Em três meses você tem uma suíte de regressão construída a partir dos erros que o seu sistema realmente comete, muito mais valiosa do que uma construída a partir do que alguém imaginou que poderia dar errado.
O pipeline
Fechando com a estrutura que sustenta tudo isso: estágios do mais rápido ao mais lento. Verificação estática em segundos. Unitários em até cinco minutos. Integração com banco em contêiner em até quinze. Ponta a ponta e deploy no fim.
Retorno em menos de dez minutos até o terceiro estágio. Acima disso, as pessoas param de rodar antes do commit.
E a regra do tronco verde: main quebrado é emergência de time. Ou reverte em minutos, ou alguém para e conserta. Ninguém sobe em cima de base quebrada.
Leia depois disto
- EngenhariaPasso 21Dados e analytics: do OLTP ao lakehouseDuas perguntas aparecem em toda empresa: por que o relatório derrubou a produção, e por que o número do meu painel é diferente do seu. As duas têm a mesma causa raiz.Ler artigo
- EngenhariaPasso 20Legado: caracterizar, costurar e estrangularTrês técnicas que permitem mexer com segurança num sistema que você não escreveu, não entende, e não pode parar.Ler artigo
- EngenhariaPasso 18OAuth2, OIDC e JWT sem mistérioAs três siglas mais confundidas de autenticação, o que cada uma resolve, e os erros que aparecem em quase todo código.Ler artigo