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.

Interpretowanie wyników testów mutacji

Po uruchomieniu Stryker.NET otrzymasz raport kategoryzujący mutanty jako zabite, przeżyte lub przekraczające limit czasu. Oto jak interpretować te wyniki i wykonywać na nich działania:

  • Zabite: są to zmiany, które zostały pomyślnie przechwycone przez testy. Duża liczba zabitych mutantów wskazuje, że zestaw testów skutecznie wykrywa błędy logiki.
  • Przetrwały: Te zmiany nie zostały przechwycone przez testy. Przejrzyj je, aby zidentyfikować luki w pokryciu testów lub stwierdzenia, które są zbyt słabe. Skoncentruj się na dodawaniu docelowych testów jednostkowych, które mogłyby zakończyć się niepowodzeniem, gdyby mutant był rzeczywisty.
  • Limit czasu: Te mutacje spowodowały zawieszenie kodu lub przekroczenie dozwolonego czasu. Może się to zdarzyć z nieskończonymi pętlami lub niezoptymalizowanymi ścieżkami. W razie potrzeby zbadaj logikę kodu lub zwiększ próg limitu czasu.

Uwaga / Notatka

Nie dąż do 100% wskaźnika mutacji. Zamiast tego skoncentruj się na obszarach o wysokim ryzyku lub krytycznym dla działania firmy, w których błędy niewykryte byłyby najbardziej kosztowne.

Dodawanie testów mutacji do przepływu pracy CI/CD

Testy mutacyjne można bezproblemowo zintegrować z procesami ciągłej integracji i dostarczania. Na przykład można skonfigurować Stryker.NET do uruchamiania w ramach konfiguracji usługi Azure Pipelines lub GitHub Actions, co umożliwia wymuszanie progów jakości w ramach procesu zautomatyzowanego testowania.

Dostosowywanie

Oprócz ustawiania progów dla potoku Stryker.NET oferuje możliwość posiadania różnych konfiguracji dla każdego z potrzeb projektu. Zachowanie można dostosować przy użyciu pliku stryker-config.json .

{
  "stryker-config": {
    "ignore-mutations": [
      "string",
      "logical"
    ],
    "ignore-methods": [
      "*Logs"
    ],
    "mutate": [
      "!**/Migrations/*",
      "!**/*.Designer.cs"
    ]
  }
}
  • ignoruj mutacje: Typy mutacji do wykluczenia z testów, ponieważ generują szum lub nie są istotne dla logiki aplikacji. Będą one wyświetlane w raportach jako Ignored.
  • ignore-methods(ignoruj metody): można użyć tej metody do pomijania całych metod na podstawie ich podpisów. Są one również wyświetlane w raportach jako Ignored. W poprzednim przykładzie wszystkie metody kończące się na ciągu "Dzienniki" są ignorowane.
  • mutate: Bez tej opcji Stryker spróbuje zmutować wszystkie pliki w projekcie. Dzięki temu można ignorować pliki lub całe foldery. W poprzednim przykładzie wszystkie elementy wewnątrz folderu Migrations i wszystkie . Designer.cs pliki (które są zwykle generowane automatycznie) są ignorowane.

Aby uzyskać więcej informacji, zobacz Stryker: Configuration (Stryker: Konfiguracja).

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.