Teste de Mutação em C#: Quebrando o Código para Melhorá-lo

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Quando os testes passam, mas o código ainda está "quebrado"

\n\n

Imagine 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?

\n\n

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\n

Neste 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\n

O que é Teste de Mutação (Mutation Testing)?

\n\n

Teste 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\n

A 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ção if (x > 5) muda para if (x >= 5), e true vira false.
  • \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
\n\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\n

Por que a cobertura de código é insuficiente?

\n\n

Vamos considerar um exemplo simples em C#. Suponha que temos um método que verifica se um número é par:

\n\n
public class MathUtils\n{\n    public bool IsEven(int number)\n    {\n        return number % 2 == 0;\n    }\n}
\n\n

E 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\n

O 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.

\n\n

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:

\n\n
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n    var utils = new MathUtils();\n    Assert.DoesNotThrow(() => utils.IsEven(4));\n}
\n\n

A 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\n

Ferramentas para Teste de Mutação em C#

\n\n

A 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\n

Outras ferramentas (menos populares):

\n