Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O teste de mutação é uma forma de avaliar a qualidade dos nossos testes unitários. Para testes de mutação, a ferramenta Stryker.NET executa automaticamente mutações em seu código, executa testes e gera um relatório detalhado com os resultados.
Exemplo de cenário de teste
Considere uma amostra PriceCalculator.cs classe com um Calculate método que calcula o preço, levando em conta o desconto.
public class PriceCalculator
{
public decimal CalculatePrice(decimal price, decimal discountPercent)
{
if (price <= 0)
{
throw new ArgumentException("Price must be greater than zero.");
}
if (discountPercent < 0 || discountPercent > 100)
{
throw new ArgumentException("Discount percent must be between 0 and 100.");
}
var discount = price * (discountPercent / 100);
var discountedPrice = price - discount;
return Math.Round(discountedPrice, 2);
}
}
O método anterior é abrangido pelos seguintes testes unitários:
[Fact]
public void ApplyDiscountCorrectly()
{
decimal price = 100;
decimal discountPercent = 10;
var calculator = new PriceCalculator();
var result = calculator.CalculatePrice(price, discountPercent);
Assert.Equal(90.00m, result);
}
[Fact]
public void InvalidDiscountPercent_ShouldThrowException()
{
var calculator = new PriceCalculator();
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
}
[Fact]
public void InvalidPrice_ShouldThrowException()
{
var calculator = new PriceCalculator();
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(-10, 10));
}
O código anterior destaca dois projetos, um para o serviço que atua como um PriceCalculator e o outro é o projeto de teste.
Instale a ferramenta global
Primeiro, instale Stryker.NET. Para fazer isso, você precisa executar o comando:
dotnet tool install -g dotnet-stryker
Para executar stryker, invoque-o a partir da linha de comando no diretório onde está localizado o projeto de teste de unidade:
dotnet stryker
Após a execução dos testes, um relatório é exibido no console.
Stryker.NET salva um relatório HTML detalhado no diretório StrykerOutput.
Agora, considere o que são mutantes e o que significa "sobreviveu" e "morreu". Um mutante é uma pequena alteração no seu código que o Stryker faz de propósito. A ideia é simples: se os seus testes forem bons, eles devem detetar a mudança e falhar. Se eles ainda passarem, seus testes podem não ser fortes o suficiente.
No nosso exemplo, um mutante será a substituição da expressão price <= 0, por exemplo, por price < 0, após o qual os testes de unidade são executados.
O Stryker suporta vários tipos de mutações:
| Tipo | Descrição |
|---|---|
| Equivalente | O operador equivalente é utilizado para substituir um operador pelo seu equivalente. Por exemplo, x < y passa a x <= y. |
| Aritmética | O operador aritmético é usado para substituir um operador aritmético pelo seu equivalente. Por exemplo, x + y passa a x - y. |
| Cordão | O operador string é usado para substituir uma string por seu equivalente. Por exemplo, "text" passa a "". |
| Lógico | O operador lógico é usado para substituir um operador lógico pelo seu equivalente. Por exemplo, x && y passa a x \|\| y. |
Para tipos de mutação adicionais, consulte a documentação Stryker.NET: Mutações .
Interpretar os resultados dos testes de mutação
Depois de correr Stryker.NET, receberá um relatório que categoriza os mutantes como eliminados, sobreviveram ou timeout. Aqui está como interpretar e agir com base nestes resultados:
- Morto: Estas são mudanças que os seus testes conseguiram detetar. Um número elevado de mutantes mortos indica que o teu conjunto de testes deteta eficazmente erros lógicos.
- Sobreviveu: Estas mudanças não foram detetadas pelos teus testes. Revê-as para identificar lacunas na cobertura dos testes ou afirmações que sejam demasiado fracas. Focar-se em adicionar testes unitários direcionados que falhariam se o mutante fosse real.
- Timeout: Estas mutações faziam com que o seu código ficasse bloqueado ou ultrapassasse o tempo permitido. Isto pode acontecer com ciclos infinitos ou caminhos não otimizados. Investigue a lógica do código ou aumente o limiar de timeout, se necessário.
Observação
Não persigas uma pontuação de mutação de 100%. Em vez disso, foque-se em áreas de alto risco ou críticas para o negócio onde os bugs não detetados seriam mais dispendiosos.
Adicionar testes de mutação ao seu fluxo de trabalho CI/CD
Pode integrar de forma fluida o teste de mutação nos seus fluxos de trabalho contínuos de integração e entrega. Por exemplo, Stryker.NET pode ser configurado para correr dentro da sua configuração Azure Pipelines ou GitHub Actions, permitindo-lhe impor limiares de qualidade como parte do seu processo automatizado de testes.
Personalização
Para além de definir limites para o seu pipeline, Stryker.NET oferece a possibilidade de ter configurações diferentes para cada uma das necessidades do seu projeto. Pode personalizar o comportamento usando o ficheiro stryker-config.json.
{
"stryker-config": {
"ignore-mutations": [
"string",
"logical"
],
"ignore-methods": [
"*Logs"
],
"mutate": [
"!**/Migrations/*",
"!**/*.Designer.cs"
]
}
}
-
Ignorar-mutações: Tipos de mutação a excluir dos testes porque são ruidosos ou não são relevantes para a lógica da sua aplicação. Eles aparecerão nos seus relatórios como
Ignored. -
ignorar-métodos: Podes usar isto para saltar métodos inteiros com base nas suas assinaturas. Estes também aparecem nos seus relatórios como
Ignored. No exemplo anterior, todos os métodos que terminam em "Logs" são ignorados. - mutação: Sem esta opção, o Stryker tentará mutar todos os ficheiros do seu projeto. Com isto, podes ignorar ficheiros ou pastas inteiras. No exemplo anterior, tudo dentro de uma pasta Migrations e todos os . Designer.cs ficheiros (que normalmente são gerados automaticamente) são ignorados.
Para mais informações, consulte Stryker: Configuração.
Melhoria incremental
Se, depois de alterar seu código, os testes de unidade passarem com sucesso, eles não serão suficientemente robustos e o mutante sobreviveu. Após o teste de mutação, cinco mutantes sobrevivem.
Vamos adicionar dados de teste para valores de limite e executar o teste de mutação novamente.
[Fact]
public void InvalidPrice_ShouldThrowException()
{
var calculator = new PriceCalculator();
// changed price from -10 to 0
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
}
[Fact] // Added test for 0 and 100 discount
public void NoExceptionForZeroAnd100Discount()
{
var calculator = new PriceCalculator();
var exceptionWhen0 = Record.Exception(() => calculator.CalculatePrice(100, 0));
var exceptionWhen100 = Record.Exception(() => calculator.CalculatePrice(100, 100));
Assert.Null(exceptionWhen0);
Assert.Null(exceptionWhen100);
}
Como você pode ver, depois de corrigir os mutantes equivalentes, só restam mutações de cordas, que podemos facilmente "matar" verificando o texto da mensagem de exceção.
[Fact]
public void InvalidDiscountPercent_ShouldThrowExceptionWithCorrectMessage()
{
var calculator = new PriceCalculator();
var ex1 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
Assert.Equal("Discount percent must be between 0 and 100.", ex1.Message);
var ex2 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
Assert.Equal("Discount percent must be between 0 and 100.", ex2.Message);
}
[Fact]
public void InvalidPrice_ShouldThrowExceptionWithCorrectMessage()
{
var calculator = new PriceCalculator();
var ex = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
Assert.Equal("Price must be greater than zero.", ex.Message);
}
Os testes de mutação ajudam a encontrar oportunidades para melhorar os testes que os tornam mais confiáveis. Ele obriga a verificar não apenas o "cenário ideal", mas também casos-limite complexos, reduzindo a probabilidade de erros em produção.