Compartilhar via


Teste de mutação

O teste de mutação é uma maneira de avaliar a qualidade de nossos testes de unidade. 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.

Cenário de teste de exemplo

Considere um exemplo 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 é coberto pelos seguintes testes de unidade:

[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 PriceCalculator e o outro é o projeto de teste.

Instalar 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 da linha de comando no diretório em que o projeto de teste de unidade está localizado:

dotnet stryker

Após a execução dos testes, um relatório é exibido no console.

Relatório do console do Stryker

Stryker.NET salva um relatório HTML detalhado no diretório StrykerOutput.

Primeiro relatório do Stryker

Agora, considere o que são mutantes e o que significa "sobreviveu" e "morto". Um mutante é uma pequena mudança no código que Stryker faz de propósito. A ideia é simples: se seus testes forem bons, eles deverão detectar a alteração e falhar. ]Se ainda assim aprovarem, talvez seus testes não sejam suficientemente rigorosos.

Em nosso exemplo, um mutante será a substituição da expressão price <= 0, por exemplo, com price < 0, após a qual os testes de unidade são executados.

O Stryker dá suporte a vários tipos de mutações:

Tipo Descrição
Equivalente O operador equivalente é usado para substituir um operador por seu equivalente. Por exemplo, x < y se tornará x <= y.
Aritmético O operador aritmético é usado para substituir um operador aritmético por seu equivalente. Por exemplo, x + y se tornará x - y.
fio O operador de cadeia de caracteres é usado para substituir uma cadeia de caracteres por seu equivalente. Por exemplo, "text" se tornará "".
Lógico O operador lógico é usado para substituir um operador lógico por seu equivalente. Por exemplo, x && y se tornará x \|\| y.

Para obter tipos de mutação adicionais, consulte a documentação Stryker.NET: Mutações .

Melhoria incremental

Se, depois de alterar o código, os testes de unidade forem aprovados com êxito, então eles não são suficientemente robustos e o mutante sobreviveu. Depois de testes 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);
}

Segundo relatório do Stryker

Como você pode ver, depois de corrigir os mutantes equivalentes, temos apenas mutações de cadeia de caracteres restantes, 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);
}

Relatório final do Stryker

O teste de mutação ajuda a encontrar oportunidades para melhorar os testes que os tornam mais confiáveis. Isso força você a verificar não apenas o "caminho feliz", mas também casos de limite complexos, reduzindo a probabilidade de bugs na produção.