Jak dobre są Twoje testy? Czyli kilka słów o testowaniu mutacyjnym.

Załóżmy, że mamy pewną aplikacje, o którą bardzo się troszczymy i robimy wszystko, żeby zapewnić jak najlepszą jakość kodu tejże aplikacji. Pokrycie kodu testami jednostkowymi jest jednym ze sposobów, którego możemy użyć, aby ‚zabezpieczyć’ nasz kod przed błędami. Dwoimy się i troimy, aż w końcu udaję nam się pokryć nasz kod w 100% i teraz mamy pewność, że żaden bug nie wkradł się do naszej aplikacji! Ale czy na pewno?

Stwórzmy aplikacje, o którą zadbamy!

Zacznijmy od ‚zewnętrznego’ serwisu, który będziemy wstrzykiwać do naszej klasy.

Teraz stwórzmy główną klasę naszej aplikacji:

Jak widzimy, nie ma tutaj żadnego ‚Rocket Science’ 🙂 Pobieramy z zewnętrznego serwisu dwie liczby, a następnie je porównujemy. Jeżeli pierwsza z nich jest większa to odejmujemy od niej drugą liczbę i otrzymaną liczbę zwracamy jako wynik. W przeciwnym przypadku jako rezultat zwracamy sumę dwóch pobranych liczb.

Pora przetestować naszą aplikacje!

Stwórzmy najpierw pomocniczą klasę, która będzie implementować interfejs naszego zewnętrznego serwisu.

I teraz pierwsza metoda testowa:

W powyższej metodzie testowej sprawdziliśmy pierwszy przypadek, czyli sytuacje gdzie pierwsza pobrana liczba z zewnętrznego serwisu jest większa od kolejnej.

Teraz pora na drugi przypadek:

Tutaj pokryliśmy drugi przypadek, czyli sytuacje gdy druga liczba jest większa od pierwszej.

Wygląda na to, że pokryliśmy wszystkie ‚branche’ i nasz kod jest w pełni przetestowany, ale skąd mamy wiedzieć jak dobre są nasze testy jednostkowe?

Testy mutacyjne

Tutaj w pomocą przychodzą nam testy mutacyjne. Czym zatem są owe testy? Jest to technika polegająca na wprowadzaniu małych i losowych zmian w kodzie naszej aplikacji. Zmiany te powinny zostać wykryte przez nasze testy jednostkowe. Jeżeli, któraś ze zmian nie została wykryta oznacza to, że nasze testy mogą nie być tak dobre jak nam się wydawało 😉

Jakie zmiany?

Poniżej znajduję się lista z przykładowymi zmianami, które mogą zostać wprowadzone w naszym kodzie.

  • Zmiana granicy w warunkach, np. > zostanie zmienione na >=, >= na >, itd.
  • Negacja warunków, np. == zostanie zmienione na !=, <= na >, itd.
  • Usunięcie warunków i zastąpienie ich stałą wartością, np. a > b zostanie zmienione na true
  • Zmiana operacji matematycznych, np. dodawanie zostanie zamienione na odejmowanie, a mnożenie na dzielenie
  • Zmiana wartości zmiennych na wartości defaultowe lub stałe, np. int zostanie ustawiony na 0 lub inną losową wartość
  • Zwrócenie null zamiast obiektu
  • Pominięcie wywołania metody typu void

Właśnie zapoznaliśmy się z przykładowymi modyfikacja, które mogą zostać wprowadzone do naszej aplikacji podczas testów mutacyjnych. Nasze testy jednostkowe powinny być napisane w taki sposób, aby zmiany te spowodowały to, że nasze testy nie przejdą.

Testy mutacyjne w praktyce

Wróćmy teraz do naszego kodu, który napisaliśmy na początku i spróbujmy przeprowadzić testy mutacyjne. Z pomocą przyjdzie nam biblioteka PIT!

Konfiguracja

Konfiguracja i uruchomienie PIT są banalnie proste! Pierwsze co musimy zrobić to dodać plugin do naszego poma:

Domyślnie wszystkie klasy z naszej aplikacji zostaną poddane testom mutacyjnym. Jeżeli chcemy to zmienić to możemy skonfigurować pakiety klas/testów, które będą wzięte pod uwagę.

Uruchomienie

Aby przeprowadzić testy mutacyjne wystarczy wywołać następujące polecenie:

Gdy operacja zakończy się sukcesem zostanie wygenerowany raport z wynikami. Znajduje się on pod następującą ścieżką: target/pit-reports/yyyyMMddHHmm.

Zmutujmy naszą aplikacje!

Pora wrócić do naszej aplikacji i wykonać na niej testy mutacyjne 🙂

Po zakończeniu testów otrzymamy wygenerowany raport.

Możemy z niego wyczytać, że nasz kod jest w pełni pokryty przez nasze testy jednostkowe (Line Coverage). Możemy również zobaczyć trochę czerwonego koloru przy pokryciu mutacyjnych testów, a jak możemy się domyślać czerwony kolor nie oznacza nic dobrego 😉

Po wklikaniu się trochę głębiej będziemy mogli zobaczyć poniższy ekran.

Możemy na nim zobaczyć, która linia naszego programu nie jest wystarczająco dobrze przetestowana, a poniżej listę mutacji, które zostały przeprowadzone w poszczególnych liniach kodu. Na zielono są zaznaczone mutacje, które zostały wykryte przez testy, natomiast na czerwono mutacje, które przeżyły i nasze testy ich nie wychwyciły.

W naszym przypadku nie została wychwycona zmiana warunku w if’ie z > na >=. Czyli w tym przypadku został wykryty warunek brzegowy, który nie został sprawdzony w testach.

Poprawy w takim razie nasz drugi test tak, aby pokrył warunek brzegowy.

Po tej modyfikacji żadne mutacje nam nie straszne i nasze testy mutacyjne przejdą na zielono 🙂

Podsumowanie

Dzisiaj zapoznaliśmy się z podstawami testów mutacyjnych. Testy te mogą nam pomóc w sprawdzeniu jak dobre są nasze testy jednostkowe. Sama koncepcja testów mutacyjnych nie jest niczym nowym, ale dopiero stosunkowo od niedawna jest używana w praktyce, ponieważ testy mutacyjne są dosyć kosztowne i wymagają sporej czasu procesora, żeby przeprowadzić wszystkie kombinacje mutacji i dopiero od niedawna nasze komputery są na tyle szybkie, żeby robić to w rozsądnym czasie 🙂