PITest – testy mutacyjne

Created with Sketch.

PITest – testy mutacyjne

Jak testować testy?

W dzisiejszych czasach nikogo już nie trzeba przekonywać do pisania testów jednostkowych. Samo pisanie dla zwiększenia pokrycia (patologia) nie ma większego sensu. Testy jednostkowe powinny być dobre! Bezwątpienia dobre testy jednostkowe to takie, które m.in wykrywają niepożądane działanie aplikacji ale i są szybkie. I to właśnie ta druga cecha pozwoliła stworzyć narzędzia do testowania mutacyjnego. W duży uproszczeniu to automatyczny proces weryfikujący czy nasze testy reagują na pojawienie się błędów w kodzie produkcyjnym.

Jednym z takich darmowych narzędzi jest PIT. Przeznaczony jest dla aplikacji napisanych w Javie . Obostrzenie to dotyczy tylko kodu produkcyjnego, testy jednostkowe mogą być napisane np. w Groovy, Kotlin, Java i z użyciem dowolnego frameworku. Do jego najważniejszych cech PITest należą:

  • bazuje na pokryciu kodu i wprowadza mutacje tylko do pokrytego testami kodu,
  • odpala tylko te testy jednostkowe które powiązane są z daną linijką kodu produkcyjnego,
  • może działać wielowątkowo,
  • wykonuje analiz przyrostową, uwzględniając w niej zmiany zarówno w testach jak i kodzie produkcyjnym,
  • mutacje wprowadza jest na poziomie bytecode co eliminuje potrzebę ciągłej re-kompilacji kodo źródłowego,
  • szybkość jego działania -wynikające z powyższych punków,
  • ma niemal zerowy koszt konfiguracji,
  • generuje czytelny raport, który rozumie Sonar,
  • uczy pisać dobre testy i pilnuje stale ich jakości.

Czym jest mutacja ?

Mutacja (z łaciny mutatio oznaczającego zmianę) to w naszym przypadku, wprowadzenie zmiany w kodzie oprogramowania w celu zmiany jego zachowania (wywołania działania niepożądanego).

Jak działają testy mutyacyjne?

W dużym uproszczeniu sposób działania testów mutacyjnych prezentuje następujący algorytm:

Dla każdej linijki pokrytej testami jednostkowymi kodu produkcyjnego wykonywane są niestępujące kroki:

  • Dla danej linijki kodu produkcyjnego wprowadzana jest mutacja.
  • Kolejnie odpalane są testy jednostkowe. Jeżeli żaden z testów nie wykrył wprowadzonej zmiany, tj. nie zakończył się niepowodzeniem, mówimy że mutacja przetrwała.
  • O ile to możliwe możemy wprowadzić rekurencyjnie kolejne mutacje
  • Kolejnie kod produkcyjny zostaje przywrócony do pierwotnej wersji
  • Jeżeli nie można wprowadzić więcej mutacji w danej linii kodu przechodzimy do kolejnej.

Odpalenie mutacji nie musi skończyć się jednym z dwóch stanów kill lub lived tych możliwości jest więcej co dokładnie jest opisane w dokumentacji PIT.

Mutacje PIT

PITest dysponuje dość dużym wachlarzem możliwości wprowadzania zmian w naszym kodzie, które można podzielić na następujace grupy:

kod produkcyjnykod po wprowadzeniu mutacji
if(a < b) doSth();if(a <= b) doSth();
if(a <= b) doSth();if(a < b) doSth();
if(a > b) doSth();if(a >= b) doSth();
if(a >= b) doSth();if(a > b) doSth();
kod produkcyjnykod po wprowadzeniu mutacji
if(a == b) doSth();if(a != b) doSth();
if(a != b) doSth();if(a == b) doSth();
if(a > b) doSth();if(a <= b) doSth();
if(a < b) doSth();if(a >= b) doSth();
if(a <= b) doSth();if(a > b) doSth();
if(a >= b) doSth();if(a < b) doSth();
kod produkcyjnykod po wprowadzeniu mutacji
if (someCondition) doSth();if(true) doSth();
if (someCondition) doSth();if (false) doSth();
kod produkcyjnykod po wprowadzeniu mutacji
var result = a + b;var result = a b;
var result = a – b;var result = a + b;
var result = a * b;var result = a / b;
var result = a / b;var result = a * b;
var result = a % b;var result = a * b;
var result = a ^ b;var result = a | b;
var result = a & b;var result = a | b;
var result = a | b;var result = a & b;
var result = a << b;var result = a >> b;
var result = a >> b;var result = a << b;
var result = a >>> b;var result = a << b;
kod produkcyjnykod po wprowadzeniu mutacji
return a++;return a–;
return a–;return a++;
typ zmiennejopis mutacji
booleannegacja przypisanej wartości, true na false i odwrotnie
int, byte, shortjeżeli zmienna ma przypisaną wartość
1 na 0,
-1 na 1,
1 do 5 na -1 0
w pozostałych przypadkach mutacja inkrementuje zmienną o jeden
longzamienia 1 na 0, w pozostałych przypadkach inkrementuje o 1
floatzmienne o wartości 1.0 i 2.0 zostaną zamienione na 0.0 w pozostałych przypadkach wartość zmiennej zostanie zamieniona na 1.0
duble 1.0 zamieniane są na 0.0 pozostałych przypadkach na 1.0
typ zwracanej wartościopis mutacji
doublenegacja zwracanej wartości
int, short, byte0 na 1, w pozostałych przypadkach 0
longzwiększa zwracaną wartość o jeden
float, doublewartość zwracanej wartości x jest zmieniana zgodnie z  –(x+1.0), jeżeli x jest NAN to x dostanie wartość 0
Objectnull zamiast obiektu
kod produkcyjnykod po wprowadzeniu mutacji
private void someFun() {…}

public void testedMethod(){
doSomething();
someFun();
}
private void someFun() {…}

public void testedMethod(){
doSomething();
}

Minimalna niezbędna konfiguracja żeby zacząć

Maven

Użycie:

Gradle

Użycie

IneliJ idea
  1. Dodaj plugin „PIT mutation testing Idea plugin”

TIP:

Jeżeli używamy junit5 musimy w definicji tasku pitest dodać

Raport

Po zakończeniu pracy w konsoli dostajemy szybkie podsumowanie z wynikiem testów mutacyjnych

Szczegółowy raport znajduje się w build / target. Domyślnie jest przejrzysty HTML.

Po otworzeniu w przeglądarce mamy już kompletny, czytelny zestaw informacji o wynikach testów. Począwszy od informacji bardzo ogólnych, takich jak globalne pokrycie, czy pokrycie mutacyjne (tj. informacja jak zachowały się nasze testy po wprowadzeniu mutacji), oraz jak sytuacja wyglada w poszczególnych pakietach.

przykładowy raport

Możemy „przeklikać” się aż do szczegółowego raportu dla poszczegółnych klas gdzie zobaczymy dokładny raport z informacjami:

  • pokryciu testami
  • lista mutacji aktywnych
  • które linijki zostały poddane mutacją
  • jakie mutacje zostały wprowadzone
  • czy i jaka mutacja przetrwała
przykładowy raport dla pojedynczej klasy

Kod z przykładem znajduje się na moim gihubie na barnachu pit-mutation-tests