3.1. Ohjelmistojen testausta
Tästä sivusta:
Pääkysymyksiä: Miksi ohjelmaa testataan? Kuinka ohjelman toimivuutta voi testata käsin ja automaattisesti?
Mitä käsitellään? Tutustutaan yksikkötestaukseen.
Mitä tehdään? Lähinnä luetaan
Suuntaa antava vaativuusarvio: Ei kovin haastava.
Suuntaa antava työläysarvio:? 1 tunti
Pistearvo: 20 pistettä
"Program testing can be used to show the presence of bugs, but never to show their absence!"
—Edsger Dijkstra
Tässä luvussa tutustumme testaukseen. Jos haluaa tutustua enemmän testaukseen, kannattaa ensiksi lukea Wikipedian artikkeli Software testing. Testaukseen liittyviä termejä kuvataan mm. tutorialspoint -sivuston testisanastossa. Testausta opetetaan tarkemmin kurssilla T-76.5613 - Software Testing and Quality Assurance.
Mitä testaus on?
Ohjelman testauksessa tutkitaan ohjelman laatua ja ominaisuuksia empiirisesti, useimmiten ohjelmaa suorittamalla. Keskeinen tarkasteltava kohde on ohjelman oikea toiminta ja olennaisin tehtävä on löytää virheitä ohjelmien toiminnassa, jotta nämä virheet voitaisiin korjata. Muita mahdollisia ominaisuuksia ovat esimerkiksi ohjelman suorituskyky, käytettävyys, toimivuus eri ympäristöissä ja tietoturva, mutta emme käsittele niitä tässä sen enempää.
Miksi testataan?
Ohjelmissa on aina virheitä eli bugeja. Virheiden syntymistä on yleensä mahdotonta estää ja ainoastaan osa niistä kyetään käytännössä havaitsemaan ennen ohjelman suorittamista. Testauksella vähennetään myös riskejä, sillä testikäytössä esiin tulevat virheet aiheuttavat yleensä vähemmän kustannuksia kuin tuotantokäytössä. Tuotantokäytössä virheet voivat olla kalliita ja jopa hengenvaarallisia, ks. List of sofware bugs Wikipediassa.
Testaus ei ole täydellistä
Ohjelmia ei yleensä voi testata täysin kattavasti. Vähänkin suuremmalla ohjelmalla on usein käytännössä rajaton määrä syötteitä ja mahdollisia sisäisiä tiloja, ei testaus kuitenkaan voi osoittaa että ohjelma olisi täysin virheetön. Testaus pyrkiikin osoittamaan että ohjelma toimii laajalti sen vaatimusmäärittelyn eli spesifikaation eli speksin (ei kuitenkaan tämän speksin.) mukaisesti ja kun se ei toimi, helpottamaan osoittamaan virheiden olemassaolon ja helpottamaan niiden paikantamista. Virhe eli bug tarkoittaa siis poikkeamaa spesifikaatiosta.
Testaukseen käytettävissä olevat resurssit ovat usein niukat. Testaus on myös työvaltaista ja tiukka aikataulu vähentää testaukseen käytettävissä olevaa aikaa. Koska täydelliseen testaukseen ei kyetä, on tärkeää huolellisesti päättää mitä asioita ohjelmassa testataan, millä syötteillä, missä järjestyksessä ja kuinka kattavasti. Tarvitaan siis priorisointia.
Milloin testataan?
Ohjelmaa on syytä testata koko sen kehityksen ajan eikä odottaa siihen asti, että ohjelma on ns. valmis. Ohjelman pieniä osasia voidaan testata heti niiden valmistuttua ja suurempia kokonaisuuksia jo ennen kaikkien osasten valmistumista. Tähän käytetään korvausolioita (test doubles), joilla mallinnetaan luokkia, metodeja ja funktioita, joita ei ole vielä toteutettu. Näistä on hyvä artikkeli Understanding test doubles.
Testitapaukset
Hyvä ohjelmankehitys yleensä tuottaa varsinaisen lähdekoodin lisäksi kokoelman huolellisesti valittuja testitapauksia. Nämä testaavat ohjelmaa sekä perustapauksilla kuten kaikkein tyypillisimmillä syötteillä ja tyypillisimmissä käyttötilanteissa että spesifikaation rajoilla ja erikoistapauksissa, kuten virheellisten syötteiden kanssa.
Testitapauksia kannattaa alkaa tuotta aikaisin, eli jo ennen kuin ohjelmakoodia on kirjoitettu. Kun ohjelmassa havaitaan jokin virhe, virheen korjaamisen lisäksi usein kirjoitetaan testi, joka olisi havainnut virheen, mikäli tällaista testiä ei jo ollut.
Kuka testaa?
Testausta tekee ohjelmakoodin kirjoittajien lisäksi erillinen testaushenkilöstö ohjelmaa tekevässä organisaatiossa. Lisäksi asiakkaalle tulevaa ohjelmaa voivat testata asiakkaan omat testaajat.
Staattinen vs. dynaaminen testaus
Useimmiten testauksesta puhuttaessa tarkoitetaan dynaamista testausta, eli ohjelman tai sen osien suorittamista sopivilla syötteillä. On myös staattista testausta, jossa ohjelmaa tai sen osia ei suoriteta. Tästä on kaksi variaatiota: ohjelman katselmointi (software review) ja staattinen analyysi. Edellisessä virheitä pyritään löytämään lukemalla ohjelmakoodia ja vertaamalla sitä dokumentaatioon. Tätä tehdään usein ryhmässä. Jälkimmäisessä käytetään erityisiä analyysityökaluja, jotka pyrkivät ohjelmaa tutkimalla löytämään siitä virheitä. Staattista testaamista käytetään vähemmän, mutta usein se olisi sangen hyödyllistä.
Testauksen tasot
Testaus jaotellaan usein erilaisiin tasoihin esim. seuraavasti:
- Yksikkötestaus: Testataan yksittäistä funktiota, luokkaa tai metodia. Tästä lisää kohdassa Yksikkötestaus.
- Integraatiotestaus: Yhdistetään joukko pienempiä ohjelman osia ja testataan niitä yhdessä.
- Järjestelmätestaus: Testataan koko ohjelmaa
- Hyväksymistestaus: Asiakas tarkistaa, täyttääkö ohjelma annetut vaatimukset.
Laatikkometaforat
Usein testauksesta käytetään laatikkometaforaa, puhutaan mustista, valkoisista ja harmaista laatikoista. Näillä tarkoitetaan seuraavaa:
- White-box: Ohjelman koodi ja mahdolliset syötteet tunnetaan ja testit voidaan laatia koodi huomioiden. Näin päästään testaamaan ohjelman sisäistä toimintaa mahdollisimman hyvin. Tämä on yleisin testauksen muoto. White-box -testausta tehdään tyypillisimmin yksikkötestauksen tai integraatiotestauksen yhteydessä.
- Black-box: Testaajalla ei ole tietoa ohjelman koodista. Ohjelmaa käsitellään "mustana laatikkona" ilman tietoa sen rakenteesta. Testaaja tietää vain, mitä ohjelman pitäisi tehdä. Testitapaukset laaditaan tällöin ohjelman vaatimusmäärittelyn perusteella. Black-box -testausta voi tehdä millä tahansa tasolla, mutta yleisimmin sitä tehdään korkeammilla tasoilla
- Grey-box: Edellisten välimuoto, jossal ohjelmasta tiedetään algoritmit ja tietorakenteet, mutta ei niiden toteutusta.
Yksikkötestaus
Yksikkötestaus on ohjelmistojen testauksen menetelmä, jossa yksittäisten ohjelman osasten toimintaa testataan, mikäli mahdollista erillään muusta koodista, jotta voidaan todeta että se täyttää sille asetetut vaatimukset. Testattava ohjelman osanen on yleensä jokin funktio, luokka tai metodi.
Yksikkötesti sisältää tyypillisesti:
- Testikoodin, joka on yleensä lyhyt
- Sanallisen kuvauksen testistä, vaikkapa tiettyjen metodien kutsuminen tietyssä järjestyksessä, esim: Add(0), Add(1), Delete(0)
- Testisyötteet
- Mahdollisesti ohjelman tilan ennen testiä
- Odotetut tulosteet ja mahdollisesti ohjelman tilan testin jälkeen.
Yksikkötestaus tehdään tyypillisesti käyttäen jotakin yksikkötestauskirjastoa, joka hoitaa testien suorittamisen ja tulosten koostamisen. Kirjastot yleensä myös toimivat yhteen IDE:jen kanssa, tehden testien ajamisesta ohjelmoinnin yhteydessä helppoa ja nopeaa. Suuremmissa projekteissa yksikkötestejä suoritetaan automaattisesti aina kun projektiin tuodaan uutta koodia. Yksikkötestauksen etuja ovatkin juuri kirjastojen tarjoama automaatio ja hyvän yksikkötestauksen tuoma varmuus testattujen luokkien toiminnasta.
Kattavuudesta
Testien laatimisessa ei pidä tyytyä vain muutamiin helpoiten mieleen tuleviin tapauksiin vaan toimia systemaattisesti ja kiinnittää huomiota testijoukon kattavuuteen. Kattavuutta voidaan arvioida useille erilaisilla koodiin ja sen suorittamiseen liittyvillä mitoilla, kuten:
- funktiokattavuus: Onko kaikkia metodeja kutsuttu?
- lausekattavuus tai rivikattavuus: Onko jokainen ohjelman lause tai rivi suoritettu?
- haarakattavuus: Onko jokaisen ehtolauseen (tai vastaavan) jokainen haara suoritettu?
- ehtokattavuus: Onko jokainen ehtolauseke evaluoitunut sekä todeksi että epätodeksi?
- polkukattavuus: Onko jokainen mahdollinen ohjelman suorituspolku suoritettu? Tätä ei yleensä ole mahdollista saavuttaa.
Näiden kattavuusmittojen selvittämiseen on olemassa valmiita työkaluja, joita voidaan käyttää testauksessa ja jotka kertovat esimerkiksi, kuinka monta prosenttia ohjelman lauseista testit käyvät läpi ja mahdollisesti näyttävät lauseet, joita testit eivät käy läpi.
Valitettavasti näiden kattavuusmittojen 100% toteutuminen (lukuunottamatta polkukattavuutta, jota puolestaan ei yleensä voi saavuttaa testeissä) ei aina yksin riitä tuottamaan ohjelman toiminnan kannalta kattavaa testijoukkoa, kuten seuraavasta esimerkistä käy ilmi.
Tehtävä: testikattavuus
Allaolevassa esimerkissä esitellään metodi suhde, joka laskee kahden kokonaisluvun suhteen mahdollisimman tarkasti ja palauttaa sen mikäli mahdollista. Mieti miten koodin tulisi toimia normaaleissa ja epänormaaleissa tapauksissa?
#
# Palauta annetuista luvuista suuremman suhde pienempään, mikäli mahdollista.
#
def suhde(n, m):
if n < m:
return m/n
else:
return n/m
Jos testijoukkomme sisältää testit:
suhde(2,4) == 2.0
suhde(4,2) == 2.0
Saadaan 100% funktiokattavuus, lausekattavuus, haarakattavuus ja ehtokattavuus. Kuitenkin jotain oleellista on jäänyt huomaamatta; mitä?
Kattavuutta pohdittaessa ei siten pidä tyytyä vain mainittujen kattavuusmittojen 100% saavuttamiseen (johon toki siihenkin pitää pyrkiä) vaan on tarkasteltava ohjelman speksiä ja pyrittävä löytämään sen avulla oleellisia testitapauksia.
Testaus ei ole aina helppoa ja suoraviivaista. Ohjelman speksi voi itsessään jo tehdä testauksesta hankalaa, mutta paljon riippuu myös ohjelman rakenteesta, esimerkiksi kuinka helppoa ohjelman osat on irroittaa toisistaan testiä varten. Monesti olion sisäinen tila riippuu suoritushistoriasta, joloin yksittäistä operaatiota testaava testi voi mennä läpi, mutta useamman operaation peräkkäinen suorittaminen epäonnistua. Esimerkiksi ohjelmaa käytettäessä havaitun virheen uudelleen toistavan testitapauksen luominen voi tällöin olla työlästä.
Rinnakkaisohjelmoinnissa suoritushistorian rooli korostuu vielä enemmän, virheet toistuvat satunnaisesti ja mahdollisesti hyvin harvoin. Tästä puhutaan lisää myöhemmin kurssilla.
Testausvinkkejä
Koodikattavuus ei siis takaa että koodi toimisi oikein, vaan sen että mahdollisesti rikkinäisen koodin joka rivillä käytiin. Testin lopputulos saattaa kuitenkin riippua vaikkapa ensimmäisestä rivistä, joka testi suoritti, vaikka kattavuusmitta näyttääkin sen suorittaneen kaikki rivit. Kuinka testitapauksia sitten kannttaisi valita? Esimerkkinä listan testaaminen:
- Normaaliarvoilla
- Esim. Laita listaan luvut 1-10 ja katso että luvut todella ovat siellä. (positiivinen testi)
- Esim. Hae listasta jossa on kymmenen alkiota alkiota joka ei ole siellä ja katso että paluuarvo on false. (negatiivinen testi)
- Sallituilla äärirajoilla
- Esim. Poista listasta alkio kun siellä on vain yksi alkio. Kaiken pitäisi sujua poikkeuksetta.
- Laittomilla äärirajoilla
- Poista alkio kun lista on tyhjä.
- Esim. Katso tuleeko koodilta speksin mukainen poikkeus. (käsitellään tarkemmin luvussa 15.1)
Muista myös että jos käytät satunnaisuutta kun luot testidataa, alusta satunnaislukugeneraattori itse vakioarvolla
- Virhetilanteet voi tällöin toistaa ja debugata
- Tai tallenna käyttämäsi siemenluku, jotta voit toistaa testit
Helpompaa testausta
Osa asioista jotka tekevät testaamisesta helpompaa, ovat myös yleisesti hyvän koodin tunnusmerkkejä, joihin tutustuimme edellisessä luvussa. Kun luokkien ja piirreluokkien rajapinnat kirjoitetaan mahdollisuuksien mukaan yksinkertaisiksi ja selkeiksi, ja luokkien välillä on vähän riippuvuuksia, niitä on helpompi testata. Kovin isot luokat kannattaa jakaa useammaksi luokaksi pyrkien erottamaan eri toiminnallisuuden osat vaikkapa omiin piirreluokkiin. Tämän jälkeen jokainen piirreluokka on tarvittaessa testattavissa erikseen.
Edellisessä luvussa esiteltiin pikaisesti muutama "sijaisluokka", joille löytyy paljon käyttöä yksikkötestauksessa, koska käytännössä luokat tarvitsevat oman toiminnallisuutensa toteuttamiseen muita luokkia. Monimutkaiseen rajapintaan on vaikea kirjoittaa oikeaa toteutusta simuloivia apuluokkia
Myös se, että luokka kutsuu käyttämiensä luokkien konstruktoreita suoraan tekee riippuvuuksien korvaamisesta haastavaa. Jos riippuvuudet vaikkapa välitetään luokan konstruktorille, ei ongelmaa tule. Vastaavasti perinnän kautta tulevat riippuvuudet on hankalampi käsitellä kuin viittausten kautta tulevat.
Test-Driven Development (TDD)
Test-driven development on testausta korostava ohjelmistokehitysprosessi, jossa kaikki lähtee liikkeelle testien kirjoittamisesta. Heti kun luokan ulkoinen rajapinta on selvillä, kirjoitetaan luokan toiminnalle spesifikaation mukaiset testit. Toimintoa ryhdytään toteuttamaan vasta kun testit ovat olemassa. Vastaavasti kun koodista löytyy virheitä, aloitetaan niiden korjaus kirjoittamalla testi joka osoittaa virheen olemassaolon ja vasta sitten ryhdytään korjaamaan. Mahdollisuus testata virhettä automaattisesti helpottaa korjausprosessia ja myöhemmin testi estää virheen paluun. Test-Driven-Development on kuvattu tarkemmin vaikkapa Wikipediassa