Quando os testes passam, mas o código ainda está "quebrado"
\n\nImagine a situação: você escreveu testes unitários para um projeto C#, dotnet test dá luz verde, a cobertura de código (code coverage) é de 85%. A equipe está satisfeita, o CI/CD roda perfeitamente. Mas um mês depois, um bug aparece em produção que, aparentemente, os testes unitários deveriam ter pego. Familiar?
O problema é que a cobertura de código (code coverage) é uma métrica enganosa. Ela mostra quais linhas de código foram tocadas, mas não verifica a qualidade das próprias verificações. Você pode ter 100% de cobertura, mas nenhum assert significativo. É aqui que entra o teste de mutação em C# — uma técnica que "quebra" seu código para verificar o quão bem seus testes conseguem capturar erros.
\n\nNeste artigo, vamos explorar o que é mutation testing, como implementá-lo em projetos .NET com Stryker.NET e por que é um must-have para qualquer desenvolvedor C# sério.
\n\nO que é Teste de Mutação (Mutation Testing)?
\n\nTeste de mutação é um método de avaliação da qualidade dos testes, baseado na introdução de pequenas alterações intencionais (mutações) no código-fonte do programa. Cada uma dessas versões modificadas do código é chamada de mutante (mutant).
\n\nA lógica é simples:
\n- \n
- Pega-se seu código de produção. \n
- Mutantes são gerados automaticamente — por exemplo, o operador
+é substituído por-, a condiçãoif (x > 5)muda paraif (x >= 5), etruevirafalse. \n - Seus testes unitários são executados contra cada mutante. \n
- Se o teste falha — o mutante é "morto" (killed). Bom teste. \n
- Se o teste passa — o mutante "sobreviveu" (survived). Mau sinal: seu teste não percebeu a mudança de lógica. \n
O objetivo é alcançar uma alta porcentagem de mutantes mortos (geralmente 80%+). Isso é uma garantia real de que seus testes verificam o comportamento, e não apenas "passam" pelo código.
\n\nPor que a cobertura de código é insuficiente?
\n\nVamos considerar um exemplo simples em C#. Suponha que temos um método que verifica se um número é par:
\n\npublic class MathUtils\n{\n public bool IsEven(int number)\n {\n return number % 2 == 0;\n }\n}\n\nE um teste "perfeito" com 100% de cobertura:
\n\n[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n var utils = new MathUtils();\n var result = utils.IsEven(4);\n Assert.IsTrue(result);\n}\n\nO teste está verde, cobertura de 100%. Mas e se alguém acidentalmente (ou maliciosamente) alterar o método para return number % 2 == 1;? Nosso único teste falhará? Não! Ele verificará o número 4 (4 % 2 == 0, mas agora o método retornará false), o assert falhará — o mutante será morto. Tudo bem? Calma.
E se alterarmos o código para return number % 2 != 1;? Logicamente é a mesma coisa, mas o teste de mutação criará um mutante: return number % 2 != 0;. Nosso teste com o número 4 ainda passará (4 % 2 != 0 — false, o método retornará false, mas o assert espera true — o teste falhará). Aqui está tudo ok. Mas imagine que escrevemos o teste assim:
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n var utils = new MathUtils();\n Assert.DoesNotThrow(() => utils.IsEven(4));\n}\n\nA cobertura ainda é de 100%, mas o assert não verifica o resultado! Agora qualquer mutante sobreviverá. O teste de mutação mostrará imediatamente que seus testes são vazios.
\n\nFerramentas para Teste de Mutação em C#
\n\nA principal e praticamente padrão ferramenta para .NET é o Stryker.NET. É um framework open-source que se integra facilmente a qualquer projeto C#. Ele suporta xUnit, NUnit e MSTest.
\n\nOutras ferramentas (menos populares):
\n- \n
- VisualMutator — plugin desatualizado para Visual Studio. \n
- Nester
Blogs
Book Recommendations
Leia também