This blog post is available in English at https://jgke.fi/blog/posts/mutation-testing/
Olet asiakkaalla, ja pöydälle tulee vaatimus funktiosta, jolla saadaan validoitua saako jonkin ikäinen ihminen juoda alkoholia baarissa. Vastaushan riippuu luonnollisesti iän lisäksi maasta. Kun kovistelit asiakasta vähän tarkemmin, sait tietää että koodin pitäisi osata vastata oikein Suomen, Yhdysvaltojen ja Saksan osalta. Ikärajat ovat vastaavasti 18, 21 ja 16 kussakin maassa.
Laitat projektin pystyyn ja alat hahmottelemaan ratkaisua Javalla.
------------------------------------------------------
------------------------------------------------------
------------------------------------------------------
Jokaiseen koodipohjaan kuuluu luonnollisesti testit. Koska asiakas on ilmoittanut deadlineksi ’eilen’, harjoitat hieman summittaista testausta ja kirjoitat muutaman testin ajattelematta tarkemmin:
------------------------------------------------------
------------------------------------------------------
Koodi ainakin tuntuu toimivan:
------------------------------------------------------
------------------------------------------------------
Et ole ihan varma, että testaako kyseiset testit kaikkea koodia. Laitat koodipohjaan simppelin testikattavuuslaskurin, ja katsot miltä näyttää:
------------------------------------------------------
------------------------------------------------------
------------------------------------------------------
Vilkaiset raporttia, jossa kaikkien rivien kohdalla lukee vihreää eli testit olivat koskeneet jokaiseen riviin. Tästä huojentuneena painat deploy-nappulaa…
…ja parin viikon päästä asiakas soittaa vihaisen puhelun, että jollekin alaikäiselle oli myyty alkoholia 1. Mikä meni pieleen?
Mutaatiotestaus
Mutaatiotestaus tarkoittaa sellaisten testaustyökalujen käyttämistä, jotka muokkaavat automaattisesti koodia ennen testien ajoa. Näiden koodimuutosten tarkoitus on rikkoa koodi, minkä pitäisi näkyä sitten testien hajoamisesta. Työkalut tämän jälkeen ilmoittavat tavallisen testikattavuuden sijaan mutaatiotestikattavuuden.
Mutaatiot tarkoittavat tässä tapauksessa koodin automaattista muokkaamista (esimerkiksi < -vertailuoperaattorin vaihtaminen > -vertailuoperaattoriksi), jonka jälkeen testien tulisi ilmoittaa virheistä.
Otetaan esimerkiksi max(a,b)-funktio, jonka toteutus on `max(a,b) = a > b ? ab. Jos pseudokoodissa olevan <-operaattorin muuttaa>` -operaattoriksi, niin koodi ei tee enää samaa kuin aiemmin, jonka testien tulisi huomata.
Javalle on saatavilla pitest-niminen kirjasto, jolla olemassa olevan testipatterin voi ajaa mutaatiotesteinä. Otetaan pitest käyttöön:
------------------------------------------------------
------------------------------------------------------
------------------------------------------------------
Terminaaliin tulee iso liuta tekstiä, joka näyttää suunnilleen seuraavalta:
------------------------------------------------------
------------------------------------------------------
Raportissa näkyvät SURVIVED 1-rivit tarkoittavat, että jotain koodiriviä muutettiin, ja testit menivät läpi – eli toisin sanottuna testit eivät oikeasti testaakaan kaikkea.
Graafisesta näkymästä näkee paremmin, mistä on kyse:
------------------------------------------------------
------------------------------------------------------
Kuvan perusteella huomataan, että koodi ei olekaan erityisen hyvin testattua, ja muutetaan testejä vastaamaan enemmän todellisuuden tarpeita:
------------------------------------------------------
------------------------------------------------------
…ajetaan testit:
------------------------------------------------------
------------------------------------------------------
…jonka jälkeen huomataan bugi alkuperäisessä koodissa:
------------------------------------------------------
------------------------------------------------------
Korjauksien jälkeen koodi toimii. Kun testit ajetaan uudelleen pitestin läpi, raportti näyttää nyt vihreämmältä!
Tässä esimerkissä saatiin mutaatiotestauksella löydettyä bugi, mitä ei alkuperäisestä koodista löytynyt tavanomaisella testikattavuuden mittauksella.
Ei pelkkää hyvää
Mutaatiotestaus, kuten kaikki muutkin työkalut, sisältää omat haittapuolensa. Selkeimmät ovat testien ajamisen hitaus sekä ajoittaiset false positive -virheet. Kuten muutkin testit, mutaatiotestaus ei myöskään takaa, että koodi toimisi.
Mutaatiotestaus myös rajoittuu testaamaan vain mutaatioita, joita kirjasto osaa tehdä koodiin. Nämä sisältävät esimerkiksi operaattorien muuttamista toisiinsa, sekä vakioiden arvojen muuttamista. Teoriassa esimerkiksi pitest mahdollistaa omien mutaatioiden lisäämisen, mutta tämä ei ole helppoa.
Testikattavuudesta
Joissakin projekteissa vaaditaan, että testikattavuus tulee pitää jonkin hatusta vedetyn rajan yläpuolella. Tämä yleensä aiheuttaa vain sen, että testeillä ylläpidetään korkeaa testikattavuutta sen sijaan, että keskityttäisiin testaamaan kunkin projektin kannalta oleelliset asiat.
Sama huomio pätee mutaatiotestaukseen. Vaikka mutaatiotestausframeworkeista saa ulos CI-ystävällisiä lukuja, se ei tarkoita sitä, että näitä tulisi käyttää sellaisenaan. Ennen kuin laitat CI:n kaatumaan jos mutaatiotestausluku on alle 100%, ajattele että onko se oikeasti tarpeellista projektin kannalta, vai olisiko hyödyllisempää käyttää testien hinkkaamiseen käytetty aika esimerkiksi dokumentaation parantamiseen.
False positive -virheet
Silloin tällöin tulee koodia vastaan, josta mutaatiotestaus antaa false positive -ilmoituksia. Yksinkertainen esimerkki on clamp-funktio:
------------------------------------------------------
------------------------------------------------------
Tähän kun kirjoittaa kourallisen testejä:
------------------------------------------------------
------------------------------------------------------
Vaikka testit ovatkin kattavat ja testaavat kaikki tapaukset, pitest ei ole tyytyväinen:
Tämä johtuu koodin luonteesta: jos x == max, ei ole väliä kumman polun koodissa ottaa. Tästä johtuen testeillä ei voi huomata <= -operaattorin muuntamista < -operaattoriksi. Näissä tilanteissa ei ole muuta vaihtoehtoa kuin merkata funktio mutaatiotestauksen ulkopuoliseksi, tai rajoittaa funktioon käytetyistä mutaatio-operaatioista pois ne operaatiot, joita ei voi testata.
Yhteenveto
Mutaatiotestaus on yksi testaustyökalu muiden joukossa. Jos kielellesi löytyy mutaatiotestauskirjasto, suosittelen sen ajamista. Älä kuitenkaan luule, että vihreät raportit tarkoittavat toimivaa koodia.
Linkkejä:
- Java: PIT mutation testing
- JavaScript, C#, Scala: Stryker Mutator
- Muita resursseja: Awesome Mutation testing