Mutation testing maakt gebruik van verschillende type mutators om onder andere operators, waarden, berekeningen en calls te wijzigen om een mutant te ceëeren. In bovenstaand voorbeeldprogramma wordt bijvoorbeeld ‘<’ vervangen door een ‘<=’ in de mutatie. De code zal fout gaan totdat de ontbrekende unit test geschreven is. In onderstaande afbeelding wordt weergegeven hoe het resultaat van de code en de ontbrekende unit test er dan uitziet.
De hierboven gebruikte mutator is een conditionals boundary mutator. Andere voorbeelden van mutators zijn een increments mutator, deze mutator verhoogt of verlaagt de waarde van een numerieke variabele. Een math mutator, deze mutator verandert de operator van + naar – of *. En een empty returns mutator, deze mutator zorgt ervoor dat return waarden van methodes of functies een lege waarde retourneren.
Tools
Zoals je waarschijnlijk al vermoedt, is mutation testing niet iets dat je handmatig doet door code te doorlopen en mutations te schrijven. Voor mutation testing zijn er veel verschillende tools op de markt, zowel commerciële producten als open source varianten. De tools moeten code goed kunnen analyseren om te bepalen wat er ontbreekt. Daarom zijn tools (meestal) geschreven voor een specifiek platform. Een aantal voorbeelden:
- Pitest (Java,Kotlin),
- Stryker (JavaScript, TypeScript, C#, Scala),
- Mutation (Ruby)
Alle beschikbare tools hebben voor- en nadelen. Er zijn drie aspecten belangrijk bij de inzetbaarheid van de tools, namelijk: snelheid, accuraatheid en ondersteuning van de betreffende programmeertaal.
Snelheid
De snelheid van de tool is afhankelijk van de mutation testing variant die gebruikt wordt. Snelheid is belangrijk omdat je zo snel mogelijk feedback wilt hebben over de kwaliteit van je code en unit testen. Er zijn in de basis twee varianten: weak mutation testing en strong mutation testing. Strong mutation testing vereist dat aan alle drie de voorwaarden van het RIP-model voldaan wordt. Bij weak mutation testing hoeft alleen maar aan de eerste voorwaarde van het RIP-model voldaan te worden, dus een test moet de gemuteerde statement bereiken. Dit kost minder tijd, bovendien is er minder rekenkracht nodig.
Daarnaast is de snelheid afhankelijk van welk type mutants gebruikt worden. Er zijn first-order mutants en higher-order mutants. First order mutants worden gegenereerd door het toepassen van mutation operators op de broncode. Door mutation operators meer dan één keer toe te passen, krijgen we higher-order mutants. Higher order mutants kun je ook zien als een combinatie van meerdere first-order mutants.
Tot slot kennen mutation testing tools verschillende configuraties, waarbij gekozen kan worden voor welke type operators ingezet gaan worden (bijvoorbeeld standard, strong, all). Hoe meer operators gebruikt worden, hoe langer het proces duurt.
Accurraatheid
Tools zoals Pitest kunnen gebruikmaken van verschillende engines, zoals Gregor en Descartes. Echter, hoe ze de code analyseren, verschilt van engine tot engine. Ze verschillen in hoe goed de code geïnterpreteerd én gemuteerd kan worden. Gregor – de standaard engine van Pitest – test ook code die niet relevant is, zoals getters en setters, en probeert ook code te muteren die in het code framework zit. Gregor muteert bijvoorbeeld je data classes en rapporteert dat deze niet goed afgedekt zijn in je testen.
Een ander voorbeeld is de Kotlin compiler. Deze voegt controles toe om je code stabieler te maken. Gregor muteert al deze automatisch gegenereerde controles en rapporteert daarom een hoop valse positieven.
Conclusie
Het doel van mutation testing is het verhogen van de kwaliteit van je unit testen. Nu wordt het controleren van unit testen vaak gedaan door middel van een code review. Mutation testing is geen vervanging van de code reviews. Tijdens de code reviews kan ook over het hoofd gezien worden dat de unit testen niet voldoen aan de business requirements en geen enkele tool helpt je daarmee.
Hoewel mutation testing veel tijd kost, is het een effectieve tool om omissies en fouten in de code te ontdekken. Het helpt je om de effectiviteit en de kwaliteit van je testen te verbeteren, waardoor je een betere testdekking krijgt en daarmee betere resultaten.
Overall is de inzet van mutation testing binnen TDD zinvol om de kwaliteit van je unit testen te verhogen. De tool keuze is hierbij erg belangrijk. Een tool die de code niet goed kan analyseren, geeft je geen inzicht in de kwaliteit van je unit testen. Het opzetten en uitvoeren van mutation testing kan veel tijd in beslag nemen. Het is dus belangrijk om de juiste afwegingen te maken. De inzet in tijd en middelen moet zich wel terugverdienen in een hogere kwaliteit. Inzet van mutation testing is dan ook logischer bij het ontwikkelen en testen van kritische software zoals besturingssoftware van vliegtuigen, medische software of betaalsystemen van banken, dan voor een simpel boekhoudpakket.