Trendsz, Test engineering, Testautomatisering, Tooling

Test Driven Development (TDD) is een veelgebruikte ontwikkelmethode waarbij eerst unit tests worden gemaakt en vervolgens de code om aan die unit tests te voldoen. Met als doel dat de code eenvoudig en efficiënt is en tegelijkertijd aan alle functionele eisen voldoet. TDD staat en valt dus met goede unit testen. Mutation testing is een manier om de kwaliteit van je unit tests te testen. Maar, helpt mutation testing daadwerkelijk bij het verbeteren van unit testen? En zo ja, in welke situatie is het slim om mutation testing toe te passen? Laten we bij het begin beginnen.

Wat is mutation testing?

Mutation testing is al bedacht eind jaren ‘70 en heeft als doel om de kwaliteit van de bestaande unit testen te verbeteren. Het idee achter mutation testing is dat we automatische bugs (mutant) maken in onze broncode en vervolgens kijken we of hierdoor een unit test faalt. Als er geen test is die faalt, dan ontbreekt er een test. Of een bestaande test moet herschreven worden om de fout te signaleren. De enige manier om ervoor te zorgen dat er geen mutants meer zijn, is het schrijven van meer unit testen. Pas als er geen mutants meer over zijn ben je klaar met unit testen. Voordat een mutant verwijderd wordt, moet er aan drie voorwaarden – beter bekend als het RIP-model – worden voldaan:

  1. Een test moet de gemuteerde statement bereiken (Reach)
  2. De gemuteerde statement moet een fout veroorzaken (Infect)
  3. De gemuteerde statement moet worden uitgevoerd en worden gecontroleerd door de test (Propagation)

Een voorbeeld van mutation testing

We nemen als voorbeeld een simpel programma dat bepaalt of het dag of nacht is. Na 7 uur ’s ochtends is het dag, daarvoor is het nacht. In onderstaande afbeelding wordt geïllustreerd op welke manier je dit kunt implementeren en hoe de unit test er dan uit kan komen zien.

VOORBEELD IMPLEMENTATIE

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.

2

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.

 

Wil je ons nieuwste Paarsz magazine per post ontvangen? Laat dan je gegevens achter.

Ontwerp zonder titel (19)

Werken bij Bartosz?

Vincent Verhelst

Geïnteresseerd in Bartosz? Dan ga ik graag met jou in gesprek. We kunnen elkaar ontmoeten met een kop koffie bij ons op kantoor. Of tijdens ontbijt, lunch, borrel of diner op een plek die jou het beste uitkomt. Jij mag het zeggen.

Mijn Paarsz