Näin yksikkötestit säästävät aikaasi

2.4.2026

Bugien implementoiminen on yllättävän helppoa, mutta miten niistä päästään eroon? Onneksi on paljon keinoja, joilla voidaan ehkäistä syntyvien bugien määrää. Yksi näistä on yksikkötestaaminen. Yksikkötestaamisella voidaan eristää tietty koodiblokki ja testata sitä hallitussa ympäristössä.

Mitkä ihmeen bugit?

Ohjelmistokehityksessä bugeista puhutaan, kun softasta löytyy vika, joka aiheuttaa ei-toivotun tuloksen. Termin alkuperä, joka suoraan suomennettuva tarkoittaa ötökkää, ei ole täysin tiedossa. Viittauksia löytyy jopa aikaan ennen 1900-lukua. Vuonna 1947 sattui tapaus, jossa kaveri nimeltä Grace Hopper löysi sen aikaisesta sähkömekaanisesta tietokoneesta vian. Vika osoittautui koiperhoseksi, joka oli jäänyt jumiin releeseen ja oli siten "bugi" systeemissä. Ötökkä teipattiin tietokoneesta printattuun lokiin ja viereen laitettiin teksti "First actual case of bug being found ". Jotkut pitävät tarinaa pötypuheena, mutta tarina osoittanee ainakin sen, että silloiset kehittäjät tiesivät termin tarkoituksen.

Miksi sinun tulisi välittää yksikkötesteistä?

Yksikkötestaaminen on suhteellisen helppo tapa tehdä laadukkaampaa koodia. Moni kehittäjä tietää, että heidän tulisi kirjoittaa enemmän yksikkötestejä. Samaan tapaan kun tiedämme, että meidän tulisi liikkua ja syödä terveellisemmin. Mutta eri asia on, kuinka usein ja systemaattisesti oikeasti teemme niin. Yksikkötestaamista ei tehdä johtoryhmää, osakkeenomistajia tai esimiehiä varten. Toki positiivisena sivuvaikutuksena voi olla edellä mainittujen sidosryhmien tyytyväisenä pitäminen. Yksikkötestaaminen on pikemminkin meitä devaajia varten. Sen lisäksi, että se auttaa saamaan bugeja kiinni, se myös dokumentoi hyvin koodin tarkoitusta.

Säästät aikaa

Yksikkötestejä tehdessäsi säästät myös aikaa. Vaikka kehitysvaiheessa ominaisuuden kehittäminen on hiukan hitaampaa, säästät kallista aikaa projektin loppupäästä. Monesti testaaminen tupataan jättämään loppuun, puhumattakaan siitä, että sitä tehdään pelkkänä käyttäjätestaamisena. Pitkän ajan kuluttua on aina hankalampaa palauttaa mieliin, mitä ja miten koodi toimi bugiin liittyvässä osassa. Jatkuvalla kehittämisen ohella tehtävässä yksikkötestaamisessa tuottavuuden ja ajan suhde on suhteellisen tasainen jana. Ilman yksikkötestejä tehokkuuden taso on alun perin hieman korkeammalla nopeamman kehittämisen puitteissa. Se todennäköisesti kuitenkin laskee kuin lehmän häntä projektin loppuvaiheessa ja voi jopa tyssätä kokonaan.

Maksa mennessäsi kaava
Yksikkötestien kanssa Pay-as-you-go tyylillä
Yksi testivaihe kaava
Ilman yksikkötestejä pelkällä lopputestauksella

Esimerkki yksikkötestistä

Otetaan esimerkkinä suhteellisen yksinkertainen funktio, joka palauttaa suurimman numeron listasta. Huomautuksena tässä kohtaa, että kaikki esimerkit ovat Javascript-koodikielellä, mutta vastaavat asiat pätevät kieleen kuin kieleen. Vain koodin syntaksi voi olla hiukan erilaista.

/**
 * Gets maximum value from a list of numbers
 */
const getMaxNumberFromList = (items: number[]): number => {
  let max = Number.MAX_VALUE
  for (let i = 0; i <= items.length; i++) {
    if (items[i] > max) max = items[i]
  }
  return max
}

Tämä funktio on aika yksiselitteinen. Se ottaa argumenttina listan numeroita ja käy sen läpi palauttaen suurimman arvon. Määritellään funktiolle seuraavaksi yksinkertainen yksikkötesti. Esimerkin mukainen testi on luotu Jest-nimisellä testauskirjastolla.

test("Returns max number from the list", () => {
  const result = getMaxNumberFromList([1, 5, 3])
  expect(result).toBe(5)
})

Jos ajat tämän testin, huomaat että se ei mene läpi: Expected: 5 Received: 1.7976931348623157e+308

Ahaa! Meillä olikin koodissa bugi. Tässä tapauksessa kyseessä oli rehellinen koodarin virhe, jossa funktion määrityksessä alustettiin muuttuja näin: let max = Number.MAX_VALUE. Eli muuttuja alustettiin suurimmaksi mahdolliseksi numeroksi, jota Javascript-koodikielessä voidaan representoida. Alustetaan arvo tämän sijaan nollaksi let max = 0 ja ajetaan yksikkötesti uudestaan. Nyt testi näyttää vihreää. Yksikkötesti on yksinkertaisimmillaan jotain tällaista. Voimme toki pistää paremmaksi!

Mitä tulisi testata?

On hyvä miettiä ennen testien kirjoittamista, mitä kaikkea tämän toiminnallisuuden osalta voitaisiin testata. Edellä näytettyä funktiota voisi testata sille annetujen argumenttien osalta tähän tapaan:

    1. Annetaan lista positiivisia kokonaislukuja [1, 5, 3] ja odotetaan palauttavan 5 -- onnistuu
    1. Annetaan lista samoja positiivisia kokonaislukuja [2, 2, 2] ja odotetaan palauttavan 2 -- onnistuu
    1. Annetaan lista, joka sisältää negatiivisen luvun [-5, 1, 3] ja odotetaan palauttavan 3 -- onnistuu
    1. Annetaan lista, joka sisältää vain negatiivisia lukuja [-5, -2, -4] ja odotetaan palauttavan -2 -- ei onnistu

Kolme ensimmäistä testiä menevät hienosti läpi, mutta neljäs ei. Tämä johtuu siitä, että maksimiarvo alustettiin funktion määrittelyssä nollaksi. Negatiiviset luvut ovat tietysti pienempiä kuin nolla. Funktio on koodattu siten, että se vertaa listan arvoja nollaa vasten, eikä siten pysty ollenkaan tunnistamaan nollan alapuolella olevia lukuja. Tässä tapauksessa voimme pysähtyä ja miettiä sitä tulisiko funktion edes pystyä käsittelemään negatiivisia lukuja. Jos tämä koodi päätyisi tuotantoon asti ja funktio saisikin pelkkiä negatiivisia arvoja tarkasteluun, olisimme aiheuttaneet bugin.

Juuri tämän takia yksikkötestaaminen on hyödyllistä, se laittaa koodarin ajattelemaan, mitkä ovat koodin raja-arvoja. Muuttakaamme funktio estämään negatiivisten lukujen syöttämisen

const getMaxNumberFromList = (items: number[]): number => {
  let max = 0
  for (let i = 0; i <= items.length; i++) {
    if (items[i] < 0) throw new Error("Negative numbers not allowed")
    if (items[i] > max) max = items[i]
  }
  return max
}

Jos ajaisimme testit nyt, niistä onnistuisivat enää kaksi ensimmäistä ja kaksi jälkimmäistä epäonnistuisivat. Itse asiassa voisimme yhdistää jälkimmäiset yhdeksi testiksi:

    1. Annetaan lista, joka sisältää negatiivisia lukuja [-5, 1, 3] ja odotetaan sen heittävän virheen

Testin määrittely olisi seuraavanlainen:

  test("Throws an exception if the list includes negative numbers", () => {
    expect(() => getMaxValueFromList([-5, 1, 3])).toThrow(
      "Negative numbers not allowed",
    )
  })

Voilà! Nyt funktiomme osaa käsitellä negatiiviset luvut ja meillä on sitä varten asianmukainen yksikkötesti. Näiden lisäksi voisimme myös miettiä, mitä tapahtuu, jos funktiota kutsuttaisiin tyhjällä listalla tai vain yhdellä arvolla.

Testaa jatkuvasti

Yksikkötestien osalta testausta olisi hyvä tehdä jatkuvasti – koko sovelluksen kehityskaaren ajan. Onkin hyvä asettaa testit ajettavaksi aina ennen uuden koodin puskemista eri ympäristöihin. Tällä tarkoitan sitä, että kehittäjä ajaa esimerkiksi uudelle featurelle luomansa testit ensin paikallisesti läpi. Sitten testit ajetaan automaattisesti uudelleen läpi, kun koodi pusketaan eteenpäin. Tällöin varmistutaan, että koodi toimii tarkoitetulla tavalla muissakin ympäristöissä. Koodipohjaa, joka sisältää epäonnistuvia testejä, ei tulisi laittaa eteenpäin, varsinkaan tuotantoon asti. Testit tulee korjata ennen tätä.

Kuvitellaanpa vielä tilanne, jossa palaat pitkän ajan kuluttua korjaamaan tunnistettua bugia. Palauttelet koodia mieleesi tai kenties et ole koskaan edes nähnyt kyseisen projektin koodipohjaa. Saat bugin korjattua ja kenties luot sitä varten uuden yksikkötestin varmistuaksesi korjauksen toiminnasta. Ajat tämän jälkeen vielä koko koodipohjan testit läpi. Nyt huomaatkin, että pieni osa testeistä muualla epäonnistuu. Tajuat, että tekemälläsi muutoksella on sivuvaikutuksia koodin toiseen osaan. Korjaat vielä hiukan koodia ja nyt kaikki testit menevät läpi onnistuneesti. Ilman yksikkötestejä muutos olisi luultavasti mennyt läpi ja mahdollinen sivuvaikutus oltaisiin huomattu vasta myöhemmin tuotannossa.

Yhteenveto

Olen nyt kenties pystynyt osoittamaan, miksi yksikkötestit ovat niin loistavia. Ne antavat sinulle koodarina varmuutta siitä, että asiat toimivat niin kuin niiden kuuluukin. Ne myös dokumentoivat koodia ja auttavat uusia kehittäjiä pääsemään paremmin kärryille koodin toiminnasta. Enkä usko, että kukaan palaa halusta päästä korjaamaan vuoren kokoista kasaa bugeja projektin lopputestauksen jäljiltä. Toki bugeja aina syntyy, oli yksikkötestejä tai ei. Niiden määrän minimoiminen on kuitenkin mahdollista!

Mitäpä jos kokeilisit seuraavassa projektissasi yksikkötestejä? Tai vaikka harjoittelisit niitä ilman erillistä projektia. Ainakin minulle niiden mukaan ottaminen on tuonut huomattavaa varmuutta omasta tekemisestä ja koodin laadusta. Tästä eteenpäin en edes lähtisi tekemään projektia, ellei minulle olisi varattu aikaa yksikkötestien toteutukseen muun toteutuksen rinnalla.

Pisteet tarkkasilmäisille

Jätin tarkoituksella koodiesimerkkiin vielä yhden bugin. Kenties joku tarkkasilmäinen huomasi tämän. Silmukassa on ehto i <= items.length, joka johtaa taulukon ulkopuoliseen indeksiin. Kyseessä on klassinen "off-by-one" virhe. Juuri tällaiset bugit ovat syy, miksi yksikkötestit ovat niin hyödyllisiä. Mieti, millaisen testin voisit kirjoittaa, jotta tämänkaltainen bugi saataisiin kiinni.

Löydä lisää aiheita