Muutakin ohjelmointia tavalliselle koodaajalle, osa 3: Oikean elämän käyttöliittymätestausta

Erika Laamanen
Orangit
Published in
5 min readJun 1, 2022

--

Edellisessä osassa loimme testin yksinkertaiselle Reducer-funktiolle, joka ottaa parametrina dataa ja palauttaa muuttuneen tilaobjektin. Tehtävä oli suhteellisen helppo niin testin laatimisen kuin vaadittavan osaamisen suhteen. Tehtävänanto ei esimerkiksi edellyttänyt juurikaan tietoa Reduxista. Reducer on vain puhdas funktio, jolla on hieno nimi. Vaatimukset olivat kohtuulliset myös sen suhteen, miten toteuttaa TDD-periaatteita jokapäiväisessä elämässä: oli kirjoitettava yksi testi, ihan mikä tahansa, kunhan sen teki ennen varsinaisen koodin kirjoittamista.

Mennään tässä tekstissä kertaheitolla toiselle tasolle. Keskitytään ensinnäkin käyttöliittymätestaukseen. Eihän Reduxillakaan yksinään tee mitään, vaan se esiintyy aina Reactin, UI:sta vastaavan kirjaston, kanssa. Tätä varten tarvitsemme Jestin lisäksi työkalun, jolla pääsemme käsiksi DOM:iin ja pystymme manipuloimaan sitä. Kiinnitetään myös huomiota siihen, mitä testataan. Ei kirjoiteta testejä siksi, että voimme sanoa tekevämme niin, vaan siksi, että niiden olemassa olosta tulee olemaan meille hyötyä.

Tehtävä, työkalut ja periaatteet

Tehtävänäni oli luoda erääseen projektiin uusi lomake, jossa kirjautunut käyttäjä voi lisätä toisille käyttäjille oikeuksia ja poistaa niitä. Lisäämisen ja poistamisen tuli tapahtua checkboxien avulla. Lähetä-nappi toimittaisi sitten muuttuneet oikeudet backendille. Miten lähestyä tehtävää testauksen näkökulmasta?

Ensinnäkin pitää valita työkalu, jolla käsitellä React-komponenttien pohjalta rakentuvaa DOM:ia. Frontendin maailmassa pelkästään sen arviointi, mikä kirjasto tukee parhaiten omia testausta koskevia periaatteita, ei riitä. Nopeasti muuttuvalla frontend-kentällä on kyettävä parhaansa mukaan ennustamaan teknologian elinvoimaisuus lähivuosina. Tällä hetkellä suosituimmat kirjastot ovat React Testing Library (RTL) ja Enzyme, joista jälkimmäisen kanssa ollaan jo ongelmissa. Enzyme vaatii rinnalleen adapterin, joka mahdollistaa Enzymen käytön kulloisessakin teknologiassa. Jokainen uusi Reactin major-versio taas vaatii oman adapterinsa. React 17:lle on odotettu adapteria siitä asti kun versio julkaistiin. Nyt kun Reactista on saatavilla versio 18, moni on jo ehtinyt julistaa Enzymen menneisyyden teknologiaksi. Jos ja kun näin todella käy, valtava määrä testejä muuttuu käyttökelvottomaksi. Itse jouduin juuri tekemään erääseen projektiin ison remontin, kun korvasin Enzymen RTL:llä.

Jos Enzyme olisi vielä varteenotettava vaihtoehto, valinta sen ja RTL:n välillä olisi periaatteellinen. Siinäkin tapauksessa liittyisin RTL:n kannattajien joukkoon. React Testing Libraryn johtava ajatus on tunnetusti se, että käyttöliittymätestauksessa olennaista on se, mitä käyttäjä näkee ja tekee. Enzyme ohjaa sen sijaan testaamaan implementaatiota: miten näkymät ja toiminnallisuudet on toteutettu. Voisi väittää, että RTL on lähempänä oikeaa elämää kuin Enzyme. Ero tuli vastaan projektissa, jossa siirryttiin käyttämään RTL:ää Enzymen sijaan. Näkymissä oli vähintään testi siitä, että sisältö ylipäätään renderöityy eli ilmestyy näytölle. Enzymen tarjoamilla välineillä laadituissa testeissä katsottiin, esiintyykö komponenteissa tietty luokkanimi:

it(‘should render correctly’, () => {
const { output } = renderShallow(TestComponent, props);
expect(output).toExists;
expect(output.props.className).toBe(‘TestComponent’);
});

Enzymen renderShallow-funktiolle annetaan React-komponentti, jota testataan, sekä komponentin ottama data eli niin kutsutut propsit. Komponentin sisältö on kääritty diviin, jolle on annettu luokkanimi (className) ’TestComponent’. Testi menee läpi, jos luokkanimi löytyy Enzymen renderöimästä komponentista.

Testi on ainakin kahdella tapaa ongelmallinen. Ensinnäkin käyttäjän näkökulmasta on samantekevää, miten koodinpätkät on nimetty. Jos kehittäjä päättää muuttaa luokkanimeä mielestään kuvaavammaksi, keskivertokäyttäjä ei ole muutoksesta tietoinen. Testi ei kuitenkaan mene enää läpi. Toisaalta testi ei ole lainkaan kiinnostunut sisällöstä, joka näytölle pitäisi ilmestyä. Sisällön käärivä div saattaa luokkanimineen renderöityä, mutta implementointivirheen takia jokin keskeinen elementti saattaa puuttua testin sitä huomaamatta. Nämä ongelmat RTL pyrkii välttämään.

Näkymä on, näkymässä on ja näkymässä tapahtuu

Palataan tehtävääni ja luodaan vaatimuksena olleelle lomakkeelle ensimmäinen testi. Aloitetaan yksinkertaisesta tilanteesta eli siitä, että lomake ylipäätään renderöityy. Voidaan tietysti kysyä, onko tällaiselle testille lainkaan tarvetta. Jonkin olemassa olon (jos ei välitetä vielä siitä, liittyykö siihen toiminnallisuuksia) toteaminen on manuaalisesti yleensä melko vaivatonta. Työhistoriassani on kuitenkin tullut vastaan tapauksia, joissa tällainen testi olisi estänyt tuotantoversion rikkoutumisen. Automaattisesti tuotantoympäristöön siirretty minor-tason riippuvuuspäivitys oli aiheuttanut tyyppivirheen (type error), minkä seurauksena sivulla näkyi pelkkää tyhjää. Tieto tästä tuli käyttäjiltä, kun taas testi olisi sen voinut kertoa ennen kuin tuotantoon siirtoa olisi ehtinyt tapahtua. Erityisesti näkymien testauksessa ’Tulisi renderöityä oikein’ -testit ovat perusteltuja.

RTL ohjaa siis vahvasti keinovalikoimillaan siirtämään huomion React-komponentista DOM:iin, implementaatiosta siihen, mitä ruudulla tulisi näkyä. Varmistaakseni, että lomakenäkymä renderöityy, luon sen RTL:n render-funktiolla ja tarkistan, että näkymän keskeiset elementit, otsikko, lomake ja Lähetä-nappi, löytyvät DOM:ista:

const props = {
header: ’Testilomake’
}
it(‘should render correctly’, () => {
render(<FormComponent {…props} />);
expect(screen.getByRole(‘heading’, { level: 1 })).toHaveTextContent(‘Testilomake’);
expect(screen.getByRole(’form’)).toBeInTheDocument();
expect(screen.getByRole(‘button’)).toBeInTheDocument();
});

Odotukset otsikkoa kohtaan voivat testin luonteeseen nähden olla liian yksityiskohtaiset: emme ainoastaan oleta, että näkymässä on otsikkoelementti, vaan vaadimme myös, että otsikko on ensimmäisen tason html-otsikko (<h1>) ja että sen tekstisisältö on muotoa ’Testilomake’. Toisaalta käytännöllisyys on korkeampi hyve kuin oikeaoppisuus. Kaikki ominaisuudet eivät vaadi omaa testilohkoaan tai testiä lainkaan. Testitkin vaativat ylläpitoa, minkä takia turhia koodirivejä on syytä välttää niidenkin yhteydessä.

Checkboxit ja niiden toimivuus sen sijaan ovat olennainen osa lomakkeen ydinajatusta ja vaativat näin ollen yksityiskohtaisempaa testausta. Checkboxeilla on ensinnäkin aloitusarvot. Lomakekomponentti saa propseina listan käyttäjistä sekä boolean-arvoja, jotka kertovat onko käyttäjillä tietty oikeus vai ei. Nuo boolean-arvot määrittävät checkboxien alkutilan. Annetaan taas RTL:n render-funktiolle FormComponent, jolle puolestaan syötetään kovakoodatut propsit, joiden perusteella voimme päätellä halutun alkutilan:

const props = {
header: ’Testilomake’,
users: [
{
name: ’user-1’,
right: true'
},
{
name: ’user-2’,
right: false
},
{
name: ’user-3’,
right: true
}
]
}
it(‘should have correct default checkbox values’, () => {
render(<FormComponent {…props} />);
const checkbox = name => screen.getByRole(‘checkbox’, { name });
expect(screen.queryAllByRole(’checkbox’)).toHaveLength(3);
expect(checkbox(’user-1')).toBeChecked();
expect(checkbox(‘user-2’)).not.toBeChecked();
expect(checkbox(‘user-3’)).toBeChecked();
});

Käyttäjiä on esimerkissämme kolme, ja ensiksi testissä tarkistetaan, että myös checkboxeja on lomakkeella kolme kappaletta. Tämän jälkeen varmistetaan, että jokaisen checkboxin aloitusarvo vastaa propsien boolean-arvoa. RTL:n getByRole-funktiolle voidaan antaa roolin lisäksi name-arvo, jolla yksilöidä elementti, jota DOM:ista etsitään. Tässä kuten aiemmassa testissämme on tietenkin ensin muistettava kirjoittaa versio, joka ei odotetusti mene läpi. Yllä olevassa testissä voidaan esimerkiksi vaihtaa viimeinen expect muotoon expect(checkbox(’user-3’)).not.toBeChecked(). Näin varmistamme, että testissä itsessään ei ole mitään vikaa.

Seuraavaksi testataan, että checkboxia klikkaamalla sen arvo vaihtuu toiseksi. Tätä varten tarvitsemme RTL:n fireEvent-funktiota:

it(‘should uncheck checkbox if checkbox checked’, () => {
render(<FormComponent {…props} />);
const checkbox = screen.getByRole(‘checkbox’, {
name: ‘user-1’
});
fireEvent.click(checkbox);
expect(checkbox).not.toBeChecked();
});

Tähän asti RTL on tarjonnut intuitiiviset välineet testata, että speksin mukaiset toiminnallisuudet toteutuvat lomakkeessa. Mitä tulee Lähetä-nappiin, päädyin kuitenkin käyttämään pelkkää Jestiä. RTL:n tapahtumien toimeenpanijoiden (fireEvent yms.) ja Jestin mock-funktioiden avulla pystyisi varsin helposti varmistamaan, että napin onClick-funktio tulee kutsutuksi (toHaveBeenCalled()), kun nappia painaa, tai että funktiota kutsutaan tiettyjen parametrien kanssa (toHaveBeenCalledWith(…)). Esimerkkitapauksessa keskeistä on kuitenkin se, mitä onClick-funktio tekee ennen datan lähettämistä backendille. Se ei nimittäin lähetä uusia boolean-arvoja vaan päätelmiä, joita se tekee boolean-arvojen pohjalta. Tuon logiikan erotin omaksi puhtaaksi funktiokseen, jota taas pystyin testaamaan samalla tavalla kuin edellisen osan Reducer-funktiota.

***

Esimerkkini on hyvin riisuttu versio tosielämän tilanteesta, jossa niin checkboxien alkutilaan kuin niistä pääteltävään tietoon liittyvä logiikka oli paljon monimutkaisempi, niin monimutkainen, että se vaati jo dokumentointia muita kehittäjiä ja tulevaisuuden itseäni varten. Testit toimivat monesti hyvänä ja usein myös riittävänä dokumentaationa, kun halutaan kirjata ylös, mitä koodilla on tarkoitus saada aikaan. Testit kirjaimellisesti kertovat, mitä pitäisi (should) näkyä ja tapahtua tai ei pitäisi (should not). Watch-moodissa kehittäjät saavat myös välittömästi palautetta, täyttääkö heidän koodinsa vaatimukset. Tiesin koodia kirjoittaessani, että implementaationi ei ollut paras mahdollinen ja että koodikatselmoinnin jälkeen joutuisin tekemään muutoksia toteutukseen. Koska refaktoroinnin jälkeenkin lomakkeen tulisi toimia niin kuin ennenkin, olin huojentunut, että olin kirjoittanut testit, jotka takaisivat koodini toimivuuden. Vaikka liikuinkin vielä käyttöliittymätestauksen perusasioissa, hyödyt olivat jo huomattavat.

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

--

--