Mutation testing

Expertises

Mutation testing,
wat houdt het in?

Testen heeft een grote invloed op de kwaliteit van software, vooral als het product volwassen wordt en de gebruikersbasis groeit. Mutatietesten helpen teams bij het beoordelen en verbeteren van de effectiviteit van de testsuite. Onze collega Marco Jongerius – Java Developer bij Pancompany – leert je in dit blog meer over Mutation testing en laat je ook een aantal voorbeelden zien.

Unit testen

Een van de belangrijkste punten tijdens het releaseproces is het zo snel mogelijk opsporen van fouten in de code. De testen die als eerste uitgevoerd worden zijn de unit testen. Om fouten tijdens de unit testen al op te kunnen sporen is het belangrijk dat de kwaliteit van de unit testen goed is.
Een van de zaken waarnaar gekeken wordt, als er wordt gesproken over de kwaliteit van de unit testen, is de code coverage van de unit testen. De code coverage geeft aan hoeveel procent van de code geraakt wordt door de aanwezige unit testen. Hoewel dit een mooi beginpunt is, om ervoor te zorgen dat er unit testen zijn voor de code, zegt dit percentage weinig tot niets over de kwaliteit van de unit testen.

Voorbeeld van een unit test

Hierboven gaf ik aan dat de code coverage weinig zegt over de kwaliteit van een test. Je vraagt je vast af hoe dit zit. In onderstaand voorbeeld leg ik het in het kort uit.

Er is een klasse BankRekening met daarop een balans. Van deze bankrekening kan geld worden opgenomen, hiervoor heeft de BankRekening de methode neemOp(int bedrag)

public class BankRekening {

    private int balans;

    public BankRekening(int startBalans){
        this.balans = startBalans;
    }

    public void neemOp(int bedrag){
        if (bedrag <= balans){
           balans = balans – bedrag;
        }
    }
}

Vervolgens wordt voor deze klasse een unit test gemaakt zodat alle regels code geraakt worden. Eén test voor het opnemen van geld als er genoeg geld op de rekening staat (voldoendeSaldoTest()) en één test voor het opnemen van geld terwijl er niet genoeg geld op de rekening staat (onvoldoendeSaldoTest()).

public class BankRekeningTest {

@Test
public void voldoendeSaldoTest(){
BankRekening bankRekening = new BankRekening(500);
bankRekening.neemOp(400);
}

@Test
public void onvoldoendeSaldoTest(){
BankRekening bankRekening = new BankRekening(500);
bankRekening.neemOp(600);
}
}

Bovenstaande voorbeeld leidt tot een unit test coverage van 100%.
Helaas is de kwaliteit van de unit test niet voldoende, want als je de volgende regel code aanpast van if (bedrag <= balans) naar if (bedrag >= balans), dan gaat de unit test nog steeds goed. Echter is het resultaat niet wenselijk, want je kan alleen nog maar geld opnemen als je meer opneemt dan op de rekening staat.

In bovenstaand voorbeeld is het natuurlijk eenvoudig om te zeggen dat er meer controles moeten worden toegevoegd aan de unit testen. Maar dat is een stuk minder overzichtelijk bij complexere methoden of voor bestaande code waar al unit testen voor zijn. Het is lastig om te zien of alle paden getest zijn. Gelukkig zijn er hiervoor mutation testing frameworks gemaakt die de unit testen automatisch controleren, ze testen eigenlijk de unit testen.

 

Mutation testing frameworks

Verschillende mutation testing frameworks werken in grote lijnen hetzelfde. Het framework genereert versies van de code met daarin per versie een wijziging in de code (mutation). De versies met daarin gewijzigde code heten mutanten. Na het creëren van de mutanten worden de unit testen gedraaid tegen dezelfde mutanten.
Als de unit test bij het draaien tegen de gewijzigde code nog steeds succesvol verloopt, dan heeft de mutant de test overleefd. Het is dan mogelijk om ongewenste wijzigingen in de code uit te voeren zonder dat er een unit test faalt. Als er wel een unit test faalt, dan is de mutant gekilled en wordt het stukje waar de mutant in is aangebracht voldoende getest.

Mutations

Zoals gezegd voert het framework een aantal mutations uit in de code. Hieronder is een lijst van een gedeelte van de mutations die worden uitgevoerd:

mutation testing

Mutation testen uitvoeren

Er zijn een aantal verschillende mutation test frameworks beschikbaar, maar in onderstaand voorbeeld wordt gebruik gemaakt van PIT (http://pitest.org). Andere frameworks die gebruikt kunnen worden zijn onder andere Jester, Jumble en Javalanche.
Om de mutation testen uit te laten voeren in een maven project moet de plugin worden toegevoegd aan de pom.xml:

mutation test 1

Hierna kan de mutation gestart worden door de volgende actie vanaf de command line uit te voeren:
mutation test 2

Rapportage

Als de mutation testen hebben gedraaid wordt er een rapportage gemaakt door de plugin. Deze is terug te vinden in de target/pit-reports folder.
Een voorbeeld van zo’n rapportage is hieronder terug te zien.

mutation testing rapportage

  • De line coverage geeft aan hoeveel regels code zijn er geraakt door de unit testen.
  • De mutation coverage geeft aan hoeveel mutanten er gekilled zijn en hoeveel mutanten er overleefd hebben. Hoe meer mutanten er gekilled zijn, des te hoger is het percentage.
  • De test strength geeft aan hoeveel mutanten er gekilled zijn of het overleefd hebben als de mutation code ook daadwerkelijk geraakt is. Het kan voorkomen dat mutanten niet geraakt worden door de unit testen. Deze mutanten worden bij de test strength buiten beschouwing gelaten.
    Vanuit de rapportage kan je doorklikken naar de betreffende packages en per klasse zien hoe door de coverage en test strength is om zo de klassen te vinden waarvoor de unit testen te verbeteren zijn.

Conclusie

Mutation testen is handig bij het verbeteren van de kwaliteit van de unit testen. Je kan eenvoudig de plekken vinden waar je unit testen kwalitatief nog te kort schieten. Zoals eerder aangegeven is het belangrijk om zo snel mogelijk fouten in de code tijdens het ontwikkelproces op te sporen. Door van tijd tot tijd de mutation testen te draaien controleer je of de unit testen kwalitatief goed zijn.

Gerelateerde berichten