Pidän itseäni pätevänä devaajana, joka on onnekkaan sattuman myötä ollut ensimmäiseksi IT-alan työkseen validointiharjoittelijana. Työ itsessään oli lähinnä automaatiotestien (useimmiten unit-testien) kirjoittamista, mutta saamani mentorointi oli ensiluokkaista. Olen viimeaikoina pureskellut näitä samoja periaatteita eteenpäin mentoroinnin muodossa ja ajattelin jakaa niitä nyt myös blogimme lukijoille.
Unit-testien tarkoitus projektissa on kaksijakoinen: toisaalta ne toimivat kuvauksena siitä, miten ohjelman osan tulee esitetyssä tilanteessa toimia, toisaalta ne toimivat ns. “sanity checkeinä” koodin muutoksille. Mikäli muutos aiheuttaa virheen aiemmin toimineessa unit-testissä, on aihetta varmistaa että muutos on todella haluttu. Testien tehokas suunnittelu on kotonaan jokaisen devaajan työkalupakissa.
Unit-testien kirjoittamisella on myös eräs piilohyöty: jos testin suunnittelu tuntuu vaikealta, tai lähes mahdottomalta, voi se johtua siitä, että testin kohde on suunniteltu huonosti ja että sitä tulisi ehkä refaktoroida.
TOIM. HUOM: Yleisesti on paljon mielipiteitä siitä, mikä lasketaan unit-testiksi ja mikä esimerkiksi integraatiotestiksi. Ei huolta. Alempia ohjeita voi helposti käyttää yleisenä ohjelmistotestien ohjenuorana.
Osa 1 - Testin sanoittaminen
“Kun tila on X ja tapahtuu Y, seurauksena on Z”
X = alkuoletukset*
Y = testin ajo
Z = ajon tuottamat tulokset
”When getSocks() function returns red socks and putOnSocks() function is called, the color of footwear changes to red”
Tämä yksinkertaistus on varsinkin aloittelevalle testin kirjoittajalle hyvä niksi hahmottaa millainen haluttu testitapaus oikeasti on. Tämä helpottaa pitämään testitapaukset selkeinä ja jakamaan monimutkaisemman toiminnallisuuden testauksen useampaan testiin.
Siirtyessämme seuraavaan osaan huomaamme myös, että tämänkaltaisesta sanoittamisesta on erityistä hyötyä kun rajaamme testiä.
Osa 2 - Testin kohteen rajaus
Testin suunnittelun kannalta on olennaista rajata, mitä halutaan testata ja mitä sulkea testauksen ulkopuolelle. Tämä rajaus kertoo meille suoraan, mikä toiminnallisuus on testin kannalta olennaista ja mikä on riippuvuus, joka voidaan yksinkertaistaa ja mallintaa (englanniksi “mock”) testin puitteissa. Matemaattisista todistuksista tuttu termi “alkuoletus” on enemmän, kuin omiaan kuvaamaan tällaista mallinnettavaa riippuvuutta.
Laajennetaan edellisen osan putOnSocks()-esimerkkiä ja oletetaan seuraavanlainen kuvaus:
getFootWearColor() {
returns the color of the worn footwear, otherwise null
}
[…]
}
const socks = {
color: randomColor()
};
return socks;
}
Alemmassa kuvassa on korostettuna, mikä on testin kannalta olennaista (eli testattavaa) toiminnallisuutta ja mockattu getSocks() funktionaalisuus, jotta se palauttaa testin kannalta halutun arvon.
getFootWearColor() {
returns the color of the worn footwear, otherwise null
}
[…]
}
putOnSocks() ja getFootwearColor() funktiot tulee säilyttää muuttumattomina (mockaamattomina), sillä molemmat ovat testin kohteena. Näistä edellinen on testattava toiminnallisuus. Jälkimmäinen taas on tuloksen tarkistamisen kannalta olennaista säilyttää koskemattomana.
randomColor = () => ‘red’
Osa 3 - Testin rakenne
Kun ylemmät osiot ovat selviä, itse testin kokoaminen on huomattavan suoraviivaista. Hyvässä testissä on tapauksesta riippuen seuraavat olennaiset osat:
- Komponentin alustus oikeaan tilaan
- Ulkoisten riippuvuuksien mockaaminen halutunlaisiksi
- Testin ajo
- Ajon vaikutusten tarkistaminen
Vaikka testissä voi olla monenlaisia rakenteita, ovat ne yleensä lopunviimein erilaisia kombinaatioita ylemmistä neljästä osasta. On aivan mahdollista, että testin kulku on esimerkiksi seuraavanlainen:
Alustus → Mockaus → Ajo → Tarkistus → Mockaus → Ajo → Tarkistus → Alustus
- Alustetaan komponentti siten, että tällä hetkellä jalassa ei ole sukkia
- Mockataan getSocks() palauttamaan punaiset sukat (kts. ylempi kuva)
- Kutsutaan putOnSocks() toiminnallisuutta
- Tarkistetaan, palauttaako getFootwearColor() ‘red’, kuten odotettu
Osa 4 - Vaaran paikkoja
Älä mockaa vääriä asioita:
Testiä tehdessä on helppo keskittyä liikaa testin läpiviemiseen ja mockata epähuomiossa vääriä asioita. Edellisessä esimerkissä putOnSocks()– tai getFootwearColor()-funktion mockaaminen olisi tehnyt testistä ilmeisen virheellisen, mutta myös riippuvuuksien kanssa kannattaa joskus olla tarkkana. Testiä tehdessäsi varmistu aina siitä, ettei mockaus vaikuta odottamattomalla tavalla ajoon tai tarkistukseen.
Onko testi järkevä:
Testitapausta sanoittaessasi osan 1 tapainen sanoitus (alkuoletus, toiminto, lopputulos) auttaa hahmottamaan kuinka järkevä testitapaus on, mutta se ei ole pakollinen.
Esimerkiksi testi, joka on sanoitettu seuraavasti…
…jossa always avainsana paljastaa, että alkutila voi olla mikä vain komponentin tila.
Vastaavasti seuraava sanoitus..
…tuntuu testitapauksena jokseenkin tyhjältä. Jos alkuoletuksen pohjalta ollaan jo käytännössä halutussa lopputilassa, on testin mielekkyys kyseenalainen.
Tarkista asiat oikein:
Testissä tehdyt tarkistukset voivat olla sinänsä ihan oikeita, mutta joskus niiden pitää myös olla riittävän tarkkoja jotta ne todella todentavat halutun tuloksen. Alla esimerkki, joka avaa mitä tarkoitan.
Jos oletetaan, että formLocked() voi heittää tietyissä tapauksissa virheen, on ok tarkistaa, että virhe myös heitetään. Mikäli funktio voi kuitenkin heittää kahta erilaista virhettä tilanteesta riippuen, on järkevää myös tarkistaa, että heitetty virhe oli juuri odotetun kaltainen.
Mikäli esimerkiksi alustuksen pohjalta odotetaan virheviestiksi…
“ERROR: Form cannot be locked”
…mutta testissä saadaankin tuloksena…
“ERROR: Form already locked”
…on testin alkutila väärä ja testin voidaan katsoa olevan virheellinen.
Testiä tehdessä mitä tarkemmin tuloksen pystyy tarkistamaan, sitä parempi!
Osa 5 - Towards an Absolute Unit
Tässä blogitekstissä on esitetty periaatteita, joita seuraamalla uudemmankin testaajan on mahdollista suunnitella testi, joka on järkevä ja hyvin rakennettu. Identiota edustaessani haluan kuitenkin osoittaa osaavani viedä ajatuksen vielä askelta pidemmälle. Katsotaanpa siis, miten teemme esimerkin putOnSocks-testistä varmemman.
Testauksessa on hyvä olla pedantti, joten lisäämme testin alkuun ns. turhan tarkistuksen, jonka tehtävänä on todentaa että komponentin alkutila on todellakin sellainen, kuin haluamme:
- Alustetaan komponentti siten, että tällä hetkellä jalassa ei ole sukkia
- Tarkistetaan, että getFootwearColor() palauttaa arvon null
- […]
Tämä tarkistus ei ole tarpeellinen, mutta se antaa meille luottoa siitä, että testin tila todellakin on se, mitä ajattelimme, emmekä ole tehneet alustuksessa virhettä.
Tämän lisäksi voimme generalisoida testin! Aloitamme muokkaamalla testikuvauksen sanoitusta seuraavanlaiseksi:
“putOnSocks() function should cause the color of footwear to change based on the color of the received socks”
Oletetaan, että putOnSocks()-funktion pitäisi myös vaihtaa päällä olevia sukkia, joten lisätään tämä osaksi testiä:
- Alustetaan komponentti siten, että tällä hetkellä jalassa ei ole sukkia
- Tarkistetaan, että getFootwearColor() palauttaa arvon null
- Mockataan getSocks() palauttamaan punaiset sukat
- Kutsutaan putOnSocks() toiminnallisuutta
- Tarkistetaan, palauttaako getFootwearColor() ‘red’, kuten odotettu
- Mockataan getSocks() palauttamaan ruskeat sukat
- Kutsutaan putOnSocks() toiminnallisuutta
- Tarkistetaan, palauttaako getFootwearColor() ‘brown’, kuten odotettu
Vaikka testiin ylimääräisten kohtien lisäämisen arvo ei ole välttämättä ilmiselvä, luo se lisää varmuutta, että ensimmäinen tarkistustulos ei ollut sattumaa. Erityisesti kompleksisissa tapauksissa on parempi tehdä liikaa tarkistuksia, kuin liian vähän. Ylempi testi ei ole hirveän monimutkainen, mutta testaa melkoisella varmuudella haluttua toiminnallisuutta.
Mikäli olet lukenut tänne asti, niin omastani ja Idention puolesta haluan kiittää mielenkiinnosta ja toivottaa hyviä hetkiä testauksen parissa!