Testy mutacyjne. O co chodzi?

Programista w podróży - testy mutacyjne

Dziś wstępnie przyjrzymy się tematowi, który nieśmiało zyskuje ostatnio popularność. Są to testy mutacyjne. W jaki sposób możemy testować nasze testy? Co nam to daje? Dlaczego testy mutacyjne są świetnym uzupełnieniem code coverage? Jak „za darmo” wygenerować nowe przypadki testowe dla naszego kodu? Jeśli zainteresowały Cię te pytania, zapraszam do lektury 😉

Co to są testy mutacyjne?

Testy mutacyjne pozwalają na testowanie naszych testów. Dokonują tego poprzez modyfikowanie (mutowanie) naszego kodu produkcyjnego. Tak zmieniony kod – czyli nasz kod produkcyjny z jedną wprowadzoną przez testy mutacyjne zmianą – nazywa się mutantem. Jest on następnie kompilowany i uruchamiane są wobec niego testy – te, które już mamy. Jeśli żaden z testów się nie wysypie, oznacza to, że nasze testy nie wyłapują zmian w kodzie produkcyjnym. Jest to zła sytuacja, w której mówimy, że mutant przeżył (survived). Jeśli natomiast co najmniej jeden test się wywali, mówimy, że mutant został zabity (killed). Jest to pożądane zachowanie, ponieważ oznacza, że zmiany w kodzie produkcyjnym są wyłapywane przez nasz zestaw testów.

Działanie testów mutacyjnych bardzo fajnie oddaje poniższy schemat:

Przykładowy wynik (output, ostatni krok na schemacie) działania narzędzia do mutowania może wyglądać tak:

Testy mutacyjne - output ze Stryker mutatora dla TypeScript

Liczba w czerwonej ramce, czyli mutanty, które przeżyły (survived), oznacza niedostatecznie pokryte testami przypadki, nad którymi będziemy pracować.

Z tego, co zauważyłem w praktyce, testy na oryginalnym kodzie są najczęściej wykonywane jeszcze przed wygenerowaniem mutantów. Framework do testów mutacyjnych wymaga więc, żeby wszystkie nasze testy przechodziły, zanim będziemy mogli uruchomić mutowanie.

Zmiany w kodzie produkcyjnym, czyli mutatory, mogą być bardzo różne. Od najprostszych mutatorów arytmetycznych, które potrafią zmienić wyrażenie a +b w a - b, przez mutatory logiczne transformujące wyrażenia typu a > b w a >= b, po bardziej złożone mutatory zmieniające sposób inicjalizacji kolekcji (np. new List { 1, 2 } staje się new List { }).

Code coverage vs testy mutacyjne

Możesz się zastanawiać, po co Ci testy mutacyjne, skoro w swoim projekcie korzystasz z code coverage. Może nawet Twoje ulubione IDE pokazuje 100% pokrycie kodu testami. Już teraz zdradzę, że testy mutacyjne nie są przeciwnikiem code coverage, ale jego świetnym uzupełnieniem.

W kolejnych artykułach o testach mutacyjnych przyjrzymy się przypadkom, gdzie 100% line coverage wcale nie oznacza całkowitego pokrycia przypadków testowych. Zobaczymy, że mimo 100% pokrycia kodu, testowanie mutacyjne pozwala znaleźć kolejne, nieprzetestowane przypadki użycia naszego kodu. W efekcie testy mutacyjne pomogą nam dopisać nowe testy naszego kodu, które poza 100% line coverage zapewnią nam także 100% mutant coverage 😎

Narzędzia do testów mutacyjnych

W każdej technologii znajdziesz inne narzędzia czy biblioteki do testów mutacyjnych. W światach JavaScript/TypeScript, C# oraz Scala popularny jest Stryker Mutator (w C# to właściwie jedyny wybór). Jednym z najlepiej rozwiniętych narzędzi do testowania mutacyjnego jest Mutant dla Ruby. W Javie będzie to Pitest.

Narzędzia różnią się stopniem rozwinięcia oraz liczbą dostępnych mutatorów. W kolejnych postach przyjrzymy się wykorzystaniu Strykera w praktyce.

Co nam dają testy mutacyjne?

Po kilku miesiącach pracy z testami mutacyjnymi widzę kilka zalet:

  • większa pewność siebie przy refactoringu. Zmieniając kod produkcyjny pokryty testami mutacyjnymi, czuję się dużo bezpieczniej
  • „darmowe” generowanie przypadków testowych. Na pewno często pisząc testy, spędzasz czas na wymyślaniu, co może pójść nie tak. Testy mutacyjne pozwalają wygenerować takie przypadki automatycznie i bez wysiłku. W efekcie dopisujesz do swojego kodu nowe testy pokrywające znalezione przez mutanty przypadki
  • wyłapywanie większej liczby błędów w kodzie produkcyjnym. Czasami wygenerowany mutant pokazuje, że jakiś warunek czy scenariusz w naszym kodzie jest bezsensowny. Skutkiem tego może być uproszczenie kodu produkcyjnego lub dopracowanie warunków brzegowych.

A wady? Ja nie widzę w zasadzie żadnych. Może poza tym, że niektóre narzędzia, jak np. Stryker dla .NETa, są jeszcze słabo rozwinięte i nie dają takich możliwości jak np. rubiowy Mutant. To pokazuje jednak, że testy mutacyjne są nadal dość świeżym tematem, którym warto zainteresować się jak najszybciej 😉

Na małą znajomość tematu wskazywały też wyniki ankiety, którą przeprowadziłem jakiś czas temu na moim Instagramie. Jeśli interesuje Cię temat testów mutacyjnych i nie chcesz przegapić kolejnych artykułów w tym temacie, zachęcam do dołączenia do mojego newslettera (maila możesz zostawić w panelu w górnej części bloga). Alternatywnie, wpadnij tutaj po prostu za kilka dni 😉

Programista, cyfrowy nomada od 2019 r. Autor bloga programistawpodrozy.pl
Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments