Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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.
Stryker.NET zapisuje szczegółowy raport HTML w katalogu StrykerOutput.
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);
}
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);
}
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.
