Udostępnij za pośrednictwem


Testowanie mutacji

Badania mutacji to sposób oceny jakości naszych testów jednostkowych. W przypadku testowania mutacji narzędzie Stryker.NET automatycznie wykonuje mutacje w kodzie, uruchamia testy i generuje szczegółowy raport z wynikami.

Przykładowy scenariusz testowy

Rozważmy przykładową klasę PriceCalculator.cs z metodą Calculate, która oblicza cenę, biorąc pod uwagę rabat.

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);
    }
}

Poprzednia metoda jest objęta następującymi testami jednostkowymi.

[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));
}

Powyższy kod wyróżnia dwa projekty: jeden dla usługi, która działa jako PriceCalculator, a druga to projekt testowy.

Instalowanie narzędzia globalnego

Najpierw zainstaluj Stryker.NET. W tym celu należy wykonać polecenie:

dotnet tool install -g dotnet-stryker

Aby uruchomić stryker, wywołaj go z wiersza polecenia w katalogu, w którym znajduje się projekt testu jednostkowego:

dotnet stryker

Po uruchomieniu testów w konsoli zostanie wyświetlony raport.

raport konsoli Stryker

Stryker.NET zapisuje szczegółowy raport HTML w katalogu StrykerOutput.

Stryker pierwszy raport

Teraz zastanów się, czym są mutanty i co "przeżył" i "zabite" oznaczają. Mutant to niewielka zmiana w kodzie, którą stryker wykonuje celowo. Pomysł jest prosty: jeśli testy są dobre, powinny wykryć zmianę i zakończyć się niepowodzeniem. Jeśli nadal przechodzą, twoje testy mogą nie być wystarczająco mocne.

W naszym przykładzie mutant zastąpi wyrażenie price <= 0, na przykład price < 0, po którym są uruchamiane testy jednostkowe.

Stryker obsługuje kilka rodzajów mutacji:

Typ Opis
Równoważny Równoważny operator jest używany do zastępowania operatora jego odpowiednikiem. Na przykład x < y staje się x <= y.
Arytmetyka Operator arytmetyczny służy do zastępowania operatora arytmetycznego jego odpowiednikiem. Na przykład x + y staje się x - y.
Sznurek Operator ciągu służy do zastępowania ciągu jego odpowiednikiem. Na przykład "text" staje się "".
Logiczny Operator logiczny jest używany do zastępowania operatora logicznego jego odpowiednikiem. Na przykład x && y staje się x \|\| y.

Aby uzyskać dodatkowe typy mutacji, sprawdź dokumentację Stryker.NET: Mutacje.

Ulepszanie przyrostowe

Jeśli po zmianie kodu testy jednostkowe zostaną pomyślnie zakończone, nie są one wystarczająco niezawodne, a mutant przetrwał. Po przetestowaniu mutacji, pięć mutantów przetrwa.

Dodajmy dane testowe dla wartości granic i ponownie uruchomimy testy mutacji.

[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);
}

Stryker drugi raport

Jak widać, po poprawieniu równoważnych mutacji, pozostały nam tylko mutacje ciągów znaków, które możemy łatwo "zabić", poprzez sprawdzenie tekstu komunikatu wyjątku.

[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);
}

raport końcowy Strykera

Testy mutacji pomagają znaleźć możliwości poprawy testów, które sprawiają, że są bardziej wiarygodne. Wymusza to sprawdzenie nie tylko "szczęśliwej ścieżki", ale także złożonych przypadków brzegowych, co zmniejsza prawdopodobieństwo występowania błędów w produkcji.