React lomakkeiden testaaminen käytännössä

Teknologia

Esitin edellisessä kirjoituksessa näkemykseni siitä miten React komponentteja kannattaa testata ja mihin testityökaluihin päädyin. Tällä kertaa sukelletaan testin ATV-rakenteeseen ja testataan React lomaketta koodiesimerkkien avulla.

Alkuvalmistelut

Testaamiseen tarvitsemme seuraavat työkalut: Jest, jest-dom, React Testing Library ja user-event. En käy tässä läpi miten nämä asennetaan tai paneudu kovin yksityiskohtaisesti siihen, miten testiajoja konfiguroidaan. Create React App -pohjaisissa sovelluksissa Jest on asennettu valmiiksi ja testiajon voi käynnistää komentoriviltä komennolla

npm run test # Suorittaa package.json tiedostossa olevan "test"-komennon

Muiden kuin Create React App -pohjaisten sovellusten kohdalla suosittelen vilkaisemaan Jestin selkeää dokumentaatiota.

# Suorittaa testejä ja jää kuuntelemaan muutoksia
# niiden kohteina oleviin tiedostoihin
jest --watch

Kirjoitan testit aina niiden komponenttien viereen, jotka ovat testauksen kohteena. LoginForm.jsx komponentin vierestä löytyy LoginForm.test.jsx niminen testitiedosto. Jest tunnistaa tällä tavalla nimetyt tiedostot ja osaa käynnistyessään ajaa niissä määritellyt testit.

// LoginForm.test.jsx
describe('LoginForm', () => { // Ryhmä, lohko tai muu vastaava!
  test('näyttää virheen kun lomake lähetetään puutteellisena', () => { // Testi
    /* Testin kaikki vaiheet tehdään täällä */
  });
});

Yksi testitiedosto voi sisältää useita testejä. Näitä voi halutessa ryhmitellä describe lohkoihin, joita itsessään voi edelleen ryhmitellä describe lohkoihin. Näin testit pysyvät järjestyksessä ja ongelmia tuottava testi on helppo paikallistaa. Jestin näppärällä komentorivityökalulla voi kätevästi suorittaa vain halutun nimiset tai tietyssä lohkossa olevat testit.

Mönkijä kiipeää rakennusmateriaalien päältä valmiin rakennelman huipulle

ATV: Aseta, Toimi, Varmista

Testauksen kulmakiveksi on vakiintunut Aseta, Toimi, Varmista -malli (englanniksi Arrange, Act, Assert).

  • Reactin kontekstissa ensin asetetaan DOM siihen tilaan, jossa sitä halutaan testata. Käytännössä tämä tarkoittaa jonkin komponentin renderöimistä.
  • Seuraavaksi toimitaan esimerkiksi painamalla painiketta tai muuttamalla lomakekentän arvoa.
  • Lopuksi varmistetaan, että toiminnasta aiheutui juuri ne seuraukset, jotka pitikin.

Aseta

React Testing Libraryn render metodilla luodaan testattava komponentti ja annetaan sille tarvittavat propit. Tämän nimenomaisen render metodin käyttö on tärkeää, koska ilman sitä emme myöhemmin pysty vuorovaikuttamaan renderöityjen elementtien kanssa.

// LoginForm.test.jsx
import { render } from '@testing-library/react';
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  test('näyttää virheen kun lomake lähetetään puutteellisena', () => {
    // Aseta: renderöidään LoginForm komponentti
    render(<LoginForm />);
  });
});

Toimi

Kirjoittamamme testin tarkoitus on varmistaa, että käyttäjälle näytetään virheilmoitus, jos lomake lähetetään puutteellisena. Puutteellisuus voi tarkoittaa käytännössä mitä haluamme; jonkin kentän arvo on tyhjä, salasana on liian lyhyt tai käyttäjätunnus ei läpäise annettua validaattoria. Kaikki riippuu siitä, miten LoginForm komponentti on toteutettu ja mitkä kaikki syötteet se hylkää (tai kääntäen, mitä haluamme sen hylkäävän, jotta testi menee läpi!). Sovitaan tässä kohtaa, että haluamme lähetyksen epäonnistuvan silloin, kun joko käyttäjätunnus- tai salasanakenttä on tyhjä. Luodaan ensin testi, jossa käyttäjätunnukselle annetaan arvo, mutta salasanalle ei.

// LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  describe('näyttää virheen, kun lomake lähetetään', () => {
    test('ilman salasanaa', () => {
      // Aseta: renderöidään LoginForm komponentti
      render(<LoginForm />);

      // Toimi: etsitään käyttäjätunnuksen syöttökenttä
      const kayttajatunnusInput = screen.getByLabelText('Käyttäjätunnus');

      // Toimi: muutetaan käyttäjätunnuskentän arvoa
      userEvent.type(kayttajatunnusInput, 'kroisos.pennonen');

      // Salasanakenttään ei kosketa!

      // Toimi: lähetetään lomake etsimällä ensin lähetä-painike
      userEvent.click(screen.getByText('Kirjaudu'));
    });
  });
});

Toimimisen perustana on React Testing Libraryn screen. Sen avulla voimme vuorovaikuttaa synteettisellä testiruudullamme olevan sisällön kanssa. Käytämme edellä paria React Testing Library querya renderöityjen elementtien löytämiseen. getByLabelText hakee elementin, jonka <label> elementin tekstisisältönä on annettu merkkijono. Voimme tehdä näin, koska LoginFormin toteuttanut koodari on antanut vastuullisesti kaikille lomakekentille labelit (tai jos ei ole, testi epäonnistuu ja sellainen pitää lisätä!). getByText puolestaan hakee elementin, jonka tekstisisältö on annettu merkkijono. Tämä soveltuu oikein hyvin painikkeiden hakemiseen.

React Testing Libraryn query-dokumentaatiossa on selkeästi kerrottu näiden hakufunktioiden toiminnasta. Suosittelen jättämään sen välilehteen auki ensimmäisiä testejä kirjoittaessasi!

Käyttäjän toimintojen matkimiseksi käytämme user-event kirjaston type ja click funktioita. Kuten kaikki user-event kirjaston funktiot, näiden vaikutukset ovat niiden nimien perusteella itsestään selviä. Testikäyttäjämme kirjoittaa käyttäjätunnuskenttään ja klikkaa ”Kirjaudu”-painiketta.

Varmista

Lopuksi haluamme varmistaa, että edellisten toimien seurauksena käyttäjälle todellakin näytetään virheilmoitus.

// LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';                      // Uusi!
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  describe('näyttää virheen, kun lomake lähetetään', () => {
    test('ilman salasanaa', () => {
      // Aseta: renderöidään LoginForm komponentti
      render(<LoginForm />);

      // Toimi: etsitään käyttäjätunnuksen syöttökenttä
      const kayttajatunnusInput = screen.getByLabelText('Käyttäjätunnus');

      // Toimi: muutetaan käyttäjätunnuskentän arvoa
      userEvent.type(kayttajatunnusInput, 'kroisos.pennonen');

      // Salasanakenttään ei kosketa!

      // Toimi: lähetetään lomake etsimällä ensin lähetä-painike
      userEvent.click(screen.getByText('Kirjaudu'));

      // Varmista: varmistetaan, että näytöltä löytyy virheestä ilmoittava teksti
      expect(
        screen.getByText('Unohtuiko salasana?'),
      ).toBeInTheDocument();
    });
  });
});

Viimeinen importimme on jest-dom. Sen avulla voimme näppärästi varmistaa kaikenlaisia DOM elementteihin liittyviä asioita. Tässä tapauksessa meille riittää pelkästään sellaisen olemassaolo, kunhan tekstisisältönä on ”Unohtuiko salasana?”.

Kas näin! Ensimmäinen testimme on paketissa. npm run test komentoriville ja se suoritetaan automaattisesti.

Jo opituilla menetelmillä voimme helposti tarkistaa myös tapaukset, joissa salasana tai molemmat lomakkeen kentät ovat tyhjiä:

// LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  describe('näyttää virheen, kun lomake lähetetään', () => {
    test('ilman salasanaa', () => {
      // Tätä testiä on hieman typistetty
      render(<LoginForm />);

      const kayttajatunnusInput = screen.getByLabelText('Käyttäjätunnus');
      userEvent.type(kayttajatunnusInput, 'kroisos.pennonen');
      userEvent.click(screen.getByText('Kirjaudu'));

      expect(
        screen.getByText('Unohtuiko salasana?'),
      ).toBeInTheDocument();
    });

    test('ilman käyttäjätunnusta', () => {
      render(<LoginForm />);

      const salasanaInput = screen.getByLabelText('Käyttäjätunnus');
      userEvent.type(salasanaInput, 'roopeAlas€€€2020');
      userEvent.click(screen.getByText('Kirjaudu'));

      expect(
        screen.getByText('Unohtuiko käyttäjätunnus?'),
      ).toBeInTheDocument();
    });

    test('tyhjänä', () => {
      render(<LoginForm />);

      userEvent.click(screen.getByText('Kirjaudu'));

      expect(
        screen.getByText('Anna nyt edes jotain tietoja!'),
      ).toBeInTheDocument();
    });
  });
});

Voit varmaankin kuvitella miltä näyttäisi testi, joka testaa lomakkeen onnistunutta lähetystä?

Jatkan tästä vielä myöhemmin kirjoituksella, jossa käyn kevyesti läpi isomman sovelluksen testaamisen haasteita ja mockausta. Mitä tehdä, kun komponentti vaatii toimiakseen jotain korkealla komponenttipuussa asuvaa dataa, kuten teeman, Routerin tai lokalisaatiotietoja? Palaan näihin tuonnempana. (Seuraa meitä Twitterissä, niin saat tiedon uusien kirjoitusten ilmestymisestä!)