Muutakin ohjelmointia tavalliselle koodaajalle, osa 2: Kirjoita yksi testi

Erika Laamanen
Orangit
Published in
5 min readNov 9, 2021

--

Joka uusivuosi joukko ohjelmistokehittäjiä vannoo tekevänsä vaadittavat parannukset työergonomiaansa sekä kirjoittavansa kaiken koodin siitä eteenpäin TDD-periaatteita noudattaen eli testivetoisesti. Loman jälkeisestä kaaoksesta on tietenkin ensin selvittävä. Kevätkin on usein tavallista kiireisempää aikaa…

Blogitekstissäni ”Kaikki koodaajat testaavat” huokailin, miten työni helpottuisi ja stressitasot laskisivat, jos kaikki koodi olisi kattavasti testattu. Riippuvuuspäivitysten automatisoinneista olisi aidosti hyötyä, kun manuaalinen testaus ei ainakaan isossa mittakaavassa olisi enää välttämätöntä. Hot reloading -moodissa testit kertoisivat reaaliajassa, jos uusi koodi tai refaktorointi rikkoisi olemassa olevia toiminnallisuuksia. Haavekuva mielessäni julistin juhlallisesti aikovani ryhdistäytyä ja kertakaikkisesti opetella kirjoittamaan niitä testejä!

Tästä on nyt melkoinen tovi, ja blogimme lukijat ovat varmasti jo ehtineet käsitellä pettymyksen tunteensa. Niin se vain menee, he joutuivat jälleen kerran toteamaan. Kuka mahtaa mitään aikapaineille ja niukille resursseille?

Epäilykset olivat aiheellisia. Tavoitteenani kun ei ollut pelkästään jonkun testaustyökalun hallitseminen tai keskeisten testausperiaatteiden omaksuminen. Päämääränäni ei ollut vähempää kuin TDD, testivetoinen kehitys, joka vertautuu pikemmin elämäntapamuutokseen kuin tietyn oppimäärän läpikäymiseen. Elämäntapamuutokset taas useammin epäonnistuvat kuin onnistuvat. Yksinkertaisuudessaan Test Driven Development tarkoittaa sitä, että ennen varsinaista koodia kirjoitetaan koodille testit. Kuulostaa helpolta, mutta niin kuulostaa jokapäiväinen juoksulenkkikin. Sen kun menee ulos. Mielikuva, jossa juoksee kevyenä lenkkipolulla, haihtuu kuitenkin ilmaan yhtä nopeasti kuin lihakset väsyvät ja hengitys tihenee — jos ulos asti pääsee. Työpäivän jälkeen valinta sohvan ja lenkin välillä on helppo mutta ei useinkaan oikea.

Frontend-kehityksessä elämäntapamuutos on kenties jyrkempi, koska käyttöliittymämuutoksia testatessa on niin luontevaa ja vaivatonta kääntyä selaimen puoleen. Hot reloading -toiminnallisuus päivittää koodimuutokset reaaliajassa myös selaimeen, ja tiuha vaihtelu sen ja koodieditorin välillä muodostuu frontend-kehittäjän työssä usein maneeriksi asti. TDD:ssä tavasta pitää oppia pois. Koodin toimivuuden monitorointi on edelleen tärkeää, mutta selaimen sijaan on totuttava kääntymään ensisijaisesti testauskäyttöliittymän puoleen.

Vaikka muutos siihenastisiin työtapoihini vaikutti radikaalilta, en lannistunut. Olin nimittäin onnistunut myös ottamaan tavaksi lähteä lenkille joka päivä. Pystyisin siis kirjoittamaan pari testiä ennen varsinaista koodia. Tai alkajaisiksi ainakin sen yhden.

Pieni askel ohjelmistokehittäjälle…

Lähdin siis liikkeelle mahdollisimman pienestä eli kirjoittamalla yhden testin TDD-periaatteita noudattaen. TDD:n työvaiheet ovat pääpiirteissään seuraavat:

  1. Kirjoita testi
  2. Testi ei mene läpi (millä varmistetaan, että testin onnistuminen todella riippuu koodista, jota testataan)
  3. Koodaa toiminnallisuus
  4. Testi menee läpi

Lisäksi pitää valita sopivat työkalut sekä tietysti kohde, jota testata. Testauskirjastoksi valitsin Jestin, koska se on laajalti käytössä työprojekteissani ja koska se soveltuu erityisen hyvin React-komponenttien testaamiseen. Testauksen kohteeksi taas valikoitui eräs React-pohjainen projekti, jossa tehtävänäni on hakea uusia entiteettejä tietokannasta, tallentaa entiteetit Redux-kirjastolla hallinnoitavaan globaaliin tilaan (state) ja lopulta näyttää ne käyttöliittymässä.

Aloitan testauksen näkökulmasta selkeästä osiosta, reducerista. Reducer-funktioiden tehtävä Reduxissa on muokata globaalia tilaa actionilta saamiensa tietojen perusteella. Reducer toisin sanoen ottaa parametrina tilan ja actionin ja palauttaa muokkaamansa uuden tilan. Tähän tarkoitukseen juuri sopivan suoraviivainen logiikka.

Oletetaan siis, että minulla on seuraava REST-kutsu: GET /api/entities. Jos kutsu onnistuu eli saan vastaukseksi Ok, aktivoituu GET_ENTITIES_SUCCESS-tyyppinen action, jolla on mukanaan vastauksena saadut entiteetit. Reducerin tehtävä on tallentaa nuo entiteetit sovelluksen tilaan.

Ensiksi luon EntityReducers.js-tiedoston, joka käsittelee entiteettejä koskevan osuuden tilasta. Luon myös reducer-funktiolle rungon, koska muuten testi kaatuu siihen, että funktiota ei löydy. Se ei taas tässä tapauksessa riitä kuittaamaan ”testi ei mene läpi” -vaihetta, koska haluan testata toiminnallisuutta, enkä sitä, onko jotain ylipäätään olemassa.

const initialState = {
entities: []
};
const EntityReducers = (entityState = initialState, action) => {
};

Tämä riittää tässä vaiheessa. Seuraavaksi luon testitiedoston EntityRecuders.test.js ja kirjoitan testin halutulle toiminnallisuudelle.

Jestin test-funktiolle riittää antaa kaksi parametria: testin nimi sekä funktio, jossa määritellään odotukset testattavalle asialle. Ensimmäisen kohdalla seuraan reducereiden yhteydessä monesti käytettyä “should handle ACTION_TYPE” -tyyppistä nimeämistapaa. Jälkimmäisessä hyödynnän Jestin omaa expect-funktiota. Expect ottaa parametrikseen arvon, jonka paikkansapitävyyttä halutaan testata, eli esimerkissäni reducer-funktion tulos. Reducer saa parametrikseen senhetkisen tilan sekä action-objektin, joka koostuu actionin tyypistä ja GET-kutsun vastauksena saaduista entiteeteistä.

Expect-funktion kanssa käytetään matcher-funktiota, jonka avulla määritellään se, mitä expect-funktiolle annetulta arvolta odotetaan. Esimerkissäni odotukset kohdistuvat reducerin palauttamaan tilaobjektiin. Tällöin sopiva matcher-funktio on toEqual, joka katsoo, onko kahdessa objektissa täsmälleen samat avain-arvo-parit. Näillä tiedoilla saan seuraavan testin:

const initialState = {
entities: []
};
test(’should handle GET_ENTITIES_SUCCESS’, () => {
const res = [
{ id: ‘1’, name: ‘abc’ },
{ id: ‘2’, name: ‘def’ }
];
const actual = reducers(initialState, {
type: ‘GET_ENTITIES_SUCCESS’,
res
});
const expected = {
…initialState,
entities: res
};
expect(actual).toEqual(expected);
});

Kun testi ajetaan, se ei odotetusti mene läpi, koska testaamaani funktio ei tee sitä, mitä sen odotetaan tekevän. Seuraavaksi kirjoitetaankin varsinainen toiminnallisuus reducer-funktioon.

​​Reducereille tyypilliseen tapaan eri action-tyypit käsitellään omassa switch-lausekkeen lohkossa. ‘GET_ENTITIES_SUCCESS’-lohkossa annetaan tilaobjektin entities-taulukolle GET-kutsun vastauksena saatu taulukko ja palautetaan sitten muuttunut tila, jossa on mukana tuo arvo:

const initialState = {
entities: []
};
const EntityReducers = (entityState = initialState, action) => {
switch (action.type) {
case ‘GET_ENTITIES_SUCCESS’:
return {
…entityState,
entities: action.res
};
}
};

Nyt kirjoittamani testi menee läpi. Ja siinä se on, TDD-periaatteella kirjoitettu koodi. Joku voi ajatella, että testattu toiminnallisuus on niin suoraviivainen, ettei se ole kaiken tuon vaivan arvoinen. Voi olla, mutta sen arviointi, mikä koodi kannattaa testata ja mikä ei, kuuluu hieman kokeneemmalle testaajalle. Aivan alkuvaiheessa on tärkeää luoda rutiineja ja jankata perusasioita niin, että ne tulevat vaikka unissaan.

Moni on varmasti myös tekstiä lukiessa useamman kerran halunnut huomauttaa osa-alueista, jotka esimerkissäni jäivät testaamatta. Entä itse GET-kutsu? Eikös se ole melkoisen tärkeä kokonaisuuden kannalta? Tai actioneiden toimivuus? Reducerin oletustila (initialState) otettiin itsestäänselvyytenä sekä se, että asiat ylipäätään ovat olemassa. Lisäksi oli puhetta käyttöliittymästä ja React-komponenteista. Niitä ei testauksen näkökulmasta käsitelty lainkaan. Jos nämä kysymykset nousivat edellisen aikana mieleesi, kannattaa jatkaa näiden blogitekstien lukemista. Saat ehkä tietää, miten edelliset ratkaisin.

Annoin aiemmin ymmärtää, että minussa on tarpeeksi itsekuria lähteä joka päivä lenkille. Se ei ollut aivan totta. En pidä liikunnasta ja ajatus siitä, että sitä pitäisi harrastaa säännöllisesti, puistattaa. Se on kuitenkin totta, että lenkkeilen päivittäin. Syy tälle on kuitenkin se, että talouteemme muutti vuosi sitten koira. Rakastan koiran kanssa ulkoilua. Saatamme jopa juosta! Se on mukavaa, ja siitä tulee hyvä olo. Silti edelleen vihaan liikuntaa. Ongelma ei kuitenkaan ole itse liikkuminen vaan sana ‘liikunta’, joka tuo mieleen koulun jumppatunnit, nuo aikataulutetut kärsimyksen hetket. Sama ilmiö pätee sanaan ’testaus’. Sille on jostain syystä muodostunut ikävä kaiku, joka saa vihaamaan itse testausta, vaikka ei olisi eläessään yhtään testiä kirjoittanut. Todellisuus voi kuitenkin olla toinen. Joskus vain tarvitsee ulkopuolisen yllykkeen ennen kuin pääsee alkuun. Oivallinen keino on ilmoittaa blogitekstissä, että aikoo opetella testaamaan ja lupautuu vielä raportoimaan tuloksista tulevissa teksteissä.

Haluaisitko liittyä joukkoomme? Etsimme uusia koodaajia tekemään ylläpidosta — ja testauksesta! — siistiä.

--

--