Pular para o conteúdo
Voltar para o acervo
Engenharia6 min de leituraPasso 19 de 22

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.

  1. Stubcontrola a entradaDevolve resposta pronta. Não verifica nada, só alimenta o caso.
  2. Fakea escolha padrãoImplementação de verdade, simplificada. Um repositório em memória de trinta linhas.
  3. Spyo real, observadoO objeto de verdade, embrulhado, registrando as chamadas.
  4. Mocktrava o desenhoVerifica interação. Refatorar deixa vinte testes vermelhos sem nenhum comportamento ter mudado.
Na dúvida entre mock e fake, prefira fake: resolve mais e quebra menos.

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

Fala comigo

Dúvida sobre o artigo? Me chama no WhatsApp

Sem formulário e sem lista de e-mail. Se você discorda de alguma coisa que eu escrevi, ou quer contar como resolveu aí, a conversa é direta comigo.

Abrir conversa