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 produkcyjny | kod 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 produkcyjny | kod 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 produkcyjny | kod po wprowadzeniu mutacji |
---|---|
if (someCondition) doSth(); | if(true) doSth(); |
if (someCondition) doSth(); | if (false) doSth(); |
kod produkcyjny | kod 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 produkcyjny | kod po wprowadzeniu mutacji |
---|---|
return a++; | return a–; |
return a–; | return a++; |
typ zmiennej | opis mutacji |
---|---|
boolean | negacja przypisanej wartości, true na false i odwrotnie |
int, byte, short | jeż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 |
long | zamienia 1 na 0, w pozostałych przypadkach inkrementuje o 1 |
float | zmienne 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ści | opis mutacji |
---|---|
double | negacja zwracanej wartości |
int, short, byte | 0 na 1, w pozostałych przypadkach 0 |
long | zwiększa zwracaną wartość o jeden |
float, double | wartość zwracanej wartości x jest zmieniana zgodnie z –(x+1.0), jeżeli x jest NAN to x dostanie wartość 0 |
Object | null zamiast obiektu |
kod produkcyjny | kod 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
1 2 3 4 5 | <plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.6.2</version> </plugin> |
Użycie:
1 | $mvn org.pitest:pitest-maven:mutationCoverage |
Gradle
1 2 3 4 5 6 7 8 9 10 11 | plugins { ... id 'info.solidsoft.pitest' version '1.5.2' } pitest { targetClasses = ['pl.boringstuff.*'] pitestVersion = '1.6.2' threads = 4 outputFormats = ['HTML'] } |
Użycie
1 | $gradle clean pitest |
IneliJ idea
- Dodaj plugin „PIT mutation testing Idea plugin”
TIP:
Jeżeli używamy junit5 musimy w definicji tasku pitest dodać
1 | junit5PluginVersion = '0.12' |
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.
1 2 | //maven target > reports > DATA_GENEROWANIA_RAPORTU > index.html |
1 2 | //gradle build > reports > DATA_GENEROWANIA_RAPORTU > index.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.

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

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