Yksikkötestaus

Testaus on jatkuvaa toimintaa ja sitä tehdään sekä osana ohjelmisto-ominaisuuden kehittämistä että automaattisesti aina, kun toiminnallisuutta lisätään etätietovarastossa päähaaraan. Tavoitteena on ns. itsensä testaava koodi, jossa ohjelmiston toiminnallisuutta kattavasti testaavat, automaattisesti ajettavissa olevat testit ovat erottamaton osa toimivaa ohjelmaa. Testaamisen ottaminen mukaan ohjelmiston kehittämisen erottamattomaksi osaksi lisää luottamusta siihen, että ohjelma on jatkuvasti toimivassa kunnossa: jos ohjelma läpäisee sille suunnitellut testit, siinä ei ole merkittäviä vikoja. Ohjelmiston tekeminen koostuu toiminnallisuuden toteuttamisen lisäksi vikojen havainnointijärjestelmän toteuttamisesta. Tällainen testaamistapa on osa jatkuvan integraation ja jatkuvan ohjelmistokehittämisen periaatteita.

Itsensä testaavan ohjelmistokehityksen merkittävä etu on, että se vähentää sellaisten bugien, jotka pääsevät huomaamatta tuotantoon saakka, määrää. Tätä tärkeämpänä etuna on, että testaamistavan avulla jokainen ohjelmiston kehittäjä voi tehdä luottavaisin mielin muutoksia ohjelmistoon pelkäämättä, että jotain menee rikki. Jos koodari tekee virheen, se saadaan jatkuvan testaamisen avulla kiinni heti ja korjattua. Kehittäjät välttyvät regressiolta eli siltä, että jonkin toiminnallisuuden lisääminen rikkoo aiempaa toiminnallisuutta. Testauksen tuoman varmuuden avulla koodin laatu paranee ja kehitystyö nopeutuu.

Siitä, ettei testaaminen ole enää ohjelmiston kehityksestä erillinen vaihe, on suoraan seurannut se, että hyvä koodari osaa testata oman koodinsa. Erityisesti yksikkötestaus on yksikön kehittäjän vastuulla: samalla, kun itse komponentti toteutetaan, toteutetaan sitä testaavat testit. Usein ohjelmistokehittäjillä on muita laadunvarmistustehtäviä muiden tiimiläisten koodiin. Esimerkiksi jokainen lisäys päähaaraan kulkee jonkun toisen tiimiläisen tekemän katselmoinnin kautta lisäyspyyntönä. Kokonaistavoite nykyaikaisessa testaamisessa on luoda kehittäjätiimin toimintatavat sellaisiksi, että toiminnallisuuden toteuttaminen kulkee käsi kädessä sitä testaavien testien toteuttamisen kanssa. Testaaminen on siten toimintakulttuurin tasolla vaikuttava asia.

Käytännön yksikkötestaus

Yksikkötestaus on siis osa yksikön toteutusvaihetta. Testit ajetaan käytännössä aina sekä toteuttajan omalla koneella paikallisesti ja sitten vielä automaattisesti etätietovarastoon lisäyksen yhteydessä osana jatkuvaa integraatiota. Yksikkötestauksessa rajapinta on usein sopiva näkymä testaukseen. Tiedon kapselointi piilottaa toteutuksen yhteneväisen rajapinnan taakse. Rajapinta on varsin muuttumaton ja komponenttia joka tapauksessa käytetään rajapinnan kautta.

Yksi tapa lähestyä yksikkötestaamista kiinteänä osana ohjelmiston kehittämistä on testivetoinen kehitys (TDD) (engl. Test Driven Development. TDD on tapa kehittää ohjelmistoja kirjaimellisesti testivetoisesti: Toiminnallisuutta testaavat testit ohjaavat koko toteutustyötä. Testivetoisessa kehityksessä toteutustyö etenee iteratiivisesti toistaen testistä toiseen samalla rakentaen lisää toiminnallisuutta ohjelmistoon:

  1. Luodaan automaattisesti ajettava testi, joka testaa seuraavaksi toteutettavaa toiminnallisuutta ja myös ajetaan se. Ensimmäisellä kerralla testi tietysti epäonnistuu.

  2. Koodataan toteutettavan ominaisuuden toiminnallinen koodi ja ajetaan sitä testaavia testejä niin kauan, että ne menevät läpi.

  3. Korjataan ja refaktoroidaan ohjelmiston koodia sen rakenteen parantamiseksi.

Kolmosvaihe on keskeinen eikä sitä saa ohittaa. Refaktorointi tarkoittaa koodin rakenteen muuttamista siten, ettei sen toiminnallisuus muutu. Se kehittää koodin ei-toiminnallista laatua, jolloin ohjelmiston ns. tekninen velka pienenee. Samalla voidaan huomata ja ratkaista monia piilossa olevia ongelmia. Tekninen velka puolestaa on metafora jokaiselle oikopolulle, nopealle ja huonosti mietitylle toteutusratkaisulle, joiden avulla saadaan ohjelmiston toiminnallisuutta lisättyä, mutta samalla sen sisäinen rakenne rapautuu. Niinkuin velka muutenkin, tekninen velka on sekä hyödyllistä (se mahdollistaa toiminnallisuuden toteuttamisen) että pakko maksaa, koska se kasvaa korkoa. Refaktorointi vähentää teknistä velkaa, koska sen kautta koodin teknistä laatua saadaan parannettua ennenkuin velan määrä on noussut hallitsemattomalle tasolle.

Yksikkötestauksen toteuttaminen

Kaikkea ei mitenkään voi testata. Esimerkiksi syötteiden arvoalueet ovat jo yksinään niin suuret, ettei edes teoreettisesti olisi mahdollista testata niitä täysin. Testitapauksia suunnitellessa pitää rajata testisyötteet sellaisiksi, että ne testaavat mahdollisimman hyvin yksikön toiminnan hallittavalla määrällä testitapauksia.

Testitapausten valintaan on hyviä käytäntöjä tarjolla. Kaksi keskeistä ovat:

  • Ekvivalenssiositus

  • Raja-arvoanalyysi

Ekvivalenssiosituksessa testitapausten muodostaminen aloitetaan jakamalla syöte ns. ekvivalenssiluokkiin. Luokat valitaan niin, että testattava kohdekoodi käsittelee kaikki luokkaan kuuluvat syötealkiot samalla tavalla. Tällöin riittää, että koodi testaan yhdellä luokan edustajalla ja sillä saatu tulos edustaa koodin toimintaa koko luokalla. Jako ekvivalenssiluokkiin tehdän tunnistamalla jokaiselle syötealkiolle (parametri, ehto) mahdollinen arvojen alue ja jaetaan se tyypillisesti perusjaolla sallittuihin ja kiellettyihin arvoihin. Kun ekvivalenssiluokkajako on saatu tarpeeksi tarkaksi, jokaisesta luokasta valitaan edustaja ja kirjoitetaan testitapaukset niitä käyttäen.

Ekvivalenssiositus

Syötteen jakautuessa useampaan ekvivalenssiluokkaan, tyypillinen virheiden aiheuttaja koodissa on syötteillä, jotka sijaitsevat luokkien reunoilla. Raja-arvoanalyysissa keskitytään testaamaan näitä reuna-alkioita. Tällaisia ovat:

  • Parametrien ja paluuarvojen reuna-arvot

  • Silmukoiden pyörimiskertojen reuna-arvot

  • Tietorakenteisiin liittyvät reuna-arvot

Esimerkiksi, jos laillinen syöte kattaa arvot 10-99, on perusteltua testata arvoilla 9, 10, 99 ja 100.

Reuna-arvojen testaaminen

Testejä kirjoittaessa on hyvä keskittyä tekemään lyhyitä selkeitä testifunktioita ja niissä yksinkertaisia tarkistuksia. Jos testeissä käytetään assertioita, ne on tärkeää sijoittaa testikoodiin. Lisäksi pitää pitää mielessä, että myös testikoodi on koodia. Se pitää siis dokumentoida, testata ja ylläpitää ihan niinkuin toiminnallinenkin koodi. Myös testikoodi voi olla virheellistä.

TEST_TYPE test_square_root() {
    double result = my_sqrt(x);
    ASSERT_TRUE((result * result) == x);
    // Pieni bugi testikoodissa (mikä?)
}

Testitapauksia keksiessä hyvä ajattelun lähtökohta on miettiä, mikä on metodille kaikkein tärkeintä ja testata aina kaikkein yleisimmät tapaukset. Kannattaa myös pyrkiä olemaan mahdollisimman luova, koska virheet löytyvät useimmin selkeiden suorituspolkujen reunamilta. Testaamisessa on hyvä keskittyä komponenttien rajapintoihin. Ne tarjoavat selkeän, ns. mustan laatikon toiminnallisuuteen. Et tiedä toiminnallisuudesta mitään, mutta rajapinnan avulla tiedät, miten palveluita tulee kutsua ja mitä niiden toiminnallisuudelta voi odottaa. Testeissä ei kannata ruveta liian mutkikkaisiin ratkaisuihin. Sen sijaan testit kannattaa pitää niin yksikertaisia kuin mahdollista. Testaamisessa on myös hyvä käyttää testikehystä. Javassa tällainen on JUnit.

Yksikkötestaus (kesto 31:15)

Staattinen analyysi

Staattisella analyysillä tarkoitetaan koodin tutkimista suorittamatta sitä. Tätä varten on ohjelmointikielille olemassa analyysityökaluja. Yksi tällainen on SonarQube. Niiden tarkoituksena on analysoida koodin yleistä kokonaislaatua ja saada kiinni koodista virheitä, joita toiminnallinen testaaminen löytää huonosti.

Staattisessa analyysissä käytetään mittareina:

  • Koodihajut (engl. code smell) ovat mikä tahansa kohta koodissa, jossa mahdollisesti piilee jokin syvempi ongelma. Hajut eivät siis välttämättä ole bugeja. Niissä ei tarvitse olla toiminnallisesti mitään vikaa. Ne kuitenkin osoittavat kooditasolla epäselvyyttä, heikkoutta tai muuta vikaa, joka joko haittaa kehittämistä tai nostaa bugiriskiä, ts. haisevat pahalle.

  • Tekninen velka (engl. technical debt) taas kuvaa tarvittavan toteutuksen parantamisen määrää. Tähän työhön lasketaan refaktorointi, toteutuksen tekninen parantaminen ja selkeyttäminen sekä muu vastaava ylläpito työ. Tekninen velka katsotaan työksi, joka jossain kohtaa on tehtävä, jos halutaan välttää ongelmia. Velka on siis maksettava takaisin.

  • Kattavuus (engl. coverage) kertoo siitä, miten laajalti yksikkötestit ajettuna testaavat toiminnallista koodia.

Analysointi paljastaa mm:

  • Alustamattomat muuttujat

  • Käyttämättömät paluuarvot

  • Virheellinen osoitinten käyttö

  • Samaa koodia useammassa kuin yhdessä paikassa

  • Koodi, jota ei koskaan ajeta eli ns. kuollut koodi

  • Koodin ylläpidettävyys- ja siirrettävyysongelmia

  • Tietoturvaongelmia

Ohjelmiston testaaminen

Yksikkötestauksessa