Mutationstest til at kvalitetssikre vores kvalitetssikring

Hos cVation arbejder vi aktivt med kvalitetssikring. Når testene går godt, kan vi læne os tilbage i stolen, klappe os selv på skuldrene og være sikre på at koden har en høj kvalitet. Eller hvad? Hvordan kan man med ro i maven stole på sine tests? Hvordan kvalitetssikrer vi vores kvalitetssikring?

Thomas Lindegaard
Software Engineer

Hos cVation arbejder vi aktivt med kvalitetssikring. Det gør vi i høj grad gennem automatiserede tests, som både hjælper os med at sikre forretningslogikken her og nu, men også beskytter os mod regressioner i fremtiden. Når vi udvikler nye features bliver de akkompagneret af en række tests som sikrer, at den nye kode fungerer som den skal i forskellige situationer. Når testene går godt, kan vi læne os tilbage i stolen, klappe os selv på skuldrene og være sikre på at koden har en høj kvalitet. Eller hvad?


Hvordan kan man med ro i maven stole på sine tests? De tests vi har lavet er jo også kode, og den kode kunne – ligesom koden vi forsøger at kvalitetssikre – indeholde fejl. Hvordan ved vi, at de automatiserede tests tester det, vi forventer? Kan vores tests overhovedet fange defekter, vi ikke selv har kunnet forudsige? Hvordan kvalitetssikrer vi vores kvalitetssikring?

Code coverage

Vi kunne skrive nye tests af vores tests, men for at undgå denne uendelige rekursion må vi finde på noget bedre. Her bruger mange code coverage. Dette koncept handler om at måle, hvor stor en del af koden, der rammes gennem de eksisterende tests. Lad os tage fat i et konkret eksempel. Denne simple metode tager imod en person og returnerer personens titel. Metoden er tiltænkt at returnere personens navn med ”Mr. ” foran, hvis personen er 21 år eller ældre:

Med code coverage kan vi undersøge om alle forgreninger i koden bliver ramt. I vores eksempel vil code coverage helt konkret kunne fortælle os, om testen dækker de scenarier hvor vi kommer ind i if-erklæringen. Med følgende test opnår vi 100% code coverage for metoden:

Her kan vi påpege to ting der illustrerer manglerne ved code coverage. For det første dækker vi kun det ene af to udfald, altså det hvor titlen præfikses med ”Mr. ”. For det andet kan vi opnå samme code coverage uden rent faktisk at teste noget som helst:

Her vil vi opnå samme 100% code coverage, men selv hvis koden har en fejl, vil testen aldrig fejle. Selv med en code coverage på 100% kan vi altså ikke erklære, at vores tests er dækkende. Code coverage kan altså påpege de områder vi bestemt ikke har testet, men værktøjet er ikke stærkt nok til at bekræfte os i, at vores tests fungerer som de skal.

Mutationstest

Som alternativ til code coverage findes der en teknik kaldet mutationstest. Med mutationstest forsøger man at måle, hvor godt man har testet ved at mutere produktionskode og fastholde den skrevne testkode. Vi prøver altså at ændre på den kode vi tester, og se om vores tests fejler som resultat. En simpel mutation kunne være at ændre ”>” til ”>=”. Hvis ændringen får en eller flere tests til at fejle siger man, at mutationen er ”død”, hvilket er godt. Hvis mutationen derimod ikke resulterer i en fejlende test, siger man, at testen har ”overlevet”. For hver overlevende mutation har man altså et sted i koden, som kan ændres uden at det bliver opdaget. Hvis man har mange overlevende mutationer, har man ikke testet sin kode godt.

Her er to eksempler på mutationer af vores kode:

Værktøjer:

Mutationstest kræver at man muterer koden og afprøver sine tests. Det giver sig selv, at hvis man skulle gøre dette i hånden ville det både være upålideligt og ineffektivt. Heldigvis findes der værktøjer, som kan udføre en lang række mutationer og afprøve tests helt automatisk. Disse værktøjer vil producere rapporter, som beskriver de overlevende mutationer.

Vores resultater med mutationstest:

I cVation brugte vi et hackathon på at eksperimentere med mutationstests. Vi brugte Stryker.NET på tre igangværende projekter. Der er forskel fra projekt til projekt og fra klasse til klasse på, hvor godt vi scorede.

Her er et delresultat hvor 76% af mutationerne døde:

Der er én metode i en klasse, der giver et pudsigt resultat. Metoden er void. Alle mutationer af selve koden inde i metoden blev dræbt af tests, men den mutant der fjerner al koden overlevede. Der er altså ingen test, der tester dén metodes sideeffekter. Det er selvfølgelig rettet nu.

Refleksion

I den nuværende version af det anvendte værktøj fandt vi et par barrierer. Blandt andet tager det lang tid at køre i en Continuous Integration pipeline, som derfor nogle gange timer ud. Det betyder naturligvis, at vi ikke kan bruge det som en gate i vores pipelines. I stedet kan man køre mutationstest dagligt og løbende analysere på resultaterne. Tiden vokser eksponentielt med mængden og kompleksiteten af kode, så en microservice-arkitektur vil reducere procestiden. Ligeledes kan man bruge det, som del af udviklingsprocessen, når man skriver ny kode. Her kan man vælge kun at mutere de områder af koden, der relaterer sig til ens arbejde.

Mutationstest kan gøre os endnu bedre til at kvalitetssikre vores software og sikre, at de tests man skriver og er afhængig af, rent faktisk er rammende og giver værdi. Når man starter med at mutationsteste kan man eksempelvis prioritere den, ved at korrelere mutation score med en hot spot analyse. På den måde kan man først arbejde med de områder i koden, som ændres ofte – i kombination med at der er mange overlevende mutanter. 

Vi er nu i gang med at implementere mutationstest på et projekt for at få erfaringer der stikker dybere end den viden vi kunne nå at opbygge på en et dags hackathon.

Læs mere om hot spot analyse i vores anden blog:
Hvordan du bruger hot spots til at prioritere jeres tekniske gæld

Teknisk gæld bliver opbygget over tid og kan af omfang være langt større end man forestiller sig. Det er nødvendigt at prioritere teknisk gæld, fordi man sjældent har tid til at håndtere al gælden på én gang.

Læs bloggen her