Os Quatro Pilares de um Bom Teste Unitário
Objective
Entender os quatro atributos que Khorikov usa para julgar qualquer teste automatizado (proteção contra regressões, resistência à refatoração, feedback rápido e manutenibilidade) e por que nenhum teste consegue maximizar os quatro ao mesmo tempo, o que é a verdadeira razão pela qual "simplesmente escreva mais testes" não é uma estratégia de teste completa.
Use Cases
- Explicar, com precisão, por que uma suíte de testes que passa 100% das vezes mas ainda deixa bugs passarem não está de fato protegendo ninguém.
- Diagnosticar uma suíte de testes que fica vermelha a cada refatoração, mesmo quando nada de fato quebrou, e nomear o que está errado nela (um problema de resistência à refatoração, não um bug no código).
- Decidir onde um novo teste deve ficar no espectro unidade/integração/ponta a ponta, raciocinando sobre qual dos quatro atributos aquela camada está mais bem posicionada para fornecer.
Deep Dive
Pilar 1: Proteção contra regressões
A capacidade de um teste de capturar um bug real depende de três coisas: quanto código roda durante o teste, quão complexo esse código é, e quão significativo ele é para o domínio de negócio. Um getter de propriedade de uma linha quase não tem onde um bug se esconder; testá-lo protege contra quase nada. Lógica de negócio complexa é o oposto: há espaço real para erro, e um bug ali é caro, então um teste que a exercita vale muito mais do que o teste do getter, mesmo que ambos sejam "um teste".
Pilar 2: Resistência à refatoração, e o falso positivo
Refatorar significa mudar código sem mudar seu comportamento observável: renomear um método, extrair uma classe. Um falso positivo é um teste que falha durante uma refatoração mesmo quando nada de fato quebrou. A história de campo do próprio livro: os testes de um time continuavam ficando vermelhos a cada tentativa de limpar código antigo (algumas falhas eram reais, a maioria não era) até que os desenvolvedores pararam de confiar na suíte por completo e começaram a desabilitar testes que falhavam por reflexo. Na próxima vez que um teste corretamente capturou um bug real, ele foi desabilitado junto com o ruído, e o bug foi para produção.
A causa raiz de falsos positivos é sempre a mesma: o teste está acoplado a detalhes de implementação em vez de comportamento observável. Um teste que afirma como o código fez algo (qual método privado foi chamado, em que ordem) quebra no momento em que esse "como" muda, mesmo quando o quê (o resultado real) continua correto.
Pilar 3: Feedback rápido
Quão rápido um teste avisa que algo está errado. Uma suíte de testes que leva uma hora para rodar é executada uma vez por dia; uma que leva dez segundos é executada a cada gravação de arquivo. Isso não é só conveniência: uma suíte lenta muda quando você descobre um problema, e o custo de corrigir um bug cresce quanto mais tempo ele fica sem ser descoberto.
Pilar 4: Manutenibilidade
Quão caro é entender e manter o teste funcionando: quão difícil é lê-lo, e quanto custa atualizá-lo toda vez que o código ao redor muda legitimamente. Um teste com muitos colaboradores para preparar e desmontar custa mais para manter do que um sem nenhum, independentemente do que ele realmente verifica.
Trade-offs
Nenhum teste consegue maximizar os quatro atributos, os três primeiros são mutuamente exclusivos: não dá para maximizar proteção contra regressões, resistência à refatoração e feedback rápido ao mesmo tempo; todo teste sacrifica um pouco de um para ganhar mais dos outros dois. Isso não é uma falha para contornar com engenharia, é um fato estrutural sobre teste que molda toda a pirâmide de testes.
Testes ponta a ponta maximizam proteção mais resistência, sacrificam velocidade: eles exercitam a maior quantidade de código (incluindo bibliotecas de terceiros e infraestrutura) e, por verificarem apenas comportamento observável externamente, são quase imunes a falsos positivos. O custo: são lentos, então uma suíte que é só de testes ponta a ponta não consegue dar feedback rápido, e poucos times conseguem se dar ao luxo de rodá-la o tempo todo.
Testes triviais maximizam resistência mais velocidade, sacrificam proteção: um teste que afirma que um getter retorna o que acabou de ser definido roda instantaneamente e essencialmente nunca dá um falso positivo, mas também não protege contra nada, já que não há espaço real naquele código para um bug se esconder:
java@Test void setterStoresName() { User user = new User(); user.setName("John Smith"); assertEquals("John Smith", user.getName()); // fast, stable, protects against ~nothing }Testes frágeis maximizam proteção mais velocidade, sacrificam resistência: um teste unitário rápido que afirma pesadamente sobre detalhes de implementação (sequências exatas de chamadas de mock, estado interno) pode genuinamente capturar regressões com rapidez, mas também falha constantemente em refatorações legítimas, que é exatamente a armadilha de falso positivo descrita pela história de campo acima.
Um teste que zera em qualquer atributo é inútil, não apenas mais fraco: como os quatro atributos se combinam multiplicativamente, e não aditivamente, um teste com ótima proteção e velocidade mas resistência zero à refatoração não é "razoavelmente bom", ele ativamente corrói a confiança na suíte inteira, o que é pior do que simplesmente não ter o teste.