Norsk

Mestre React Testing Library (RTL) med denne komplette guiden. Lær hvordan du skriver effektive, vedlikeholdbare og brukersentriske tester for dine React-applikasjoner.

React Testing Library: En omfattende guide

I dagens fartsfylte webutviklingslandskap er det avgjørende å sikre kvaliteten og påliteligheten til dine React-applikasjoner. React Testing Library (RTL) har dukket opp som en populær og effektiv løsning for å skrive tester som fokuserer på brukerens perspektiv. Denne guiden gir en komplett oversikt over RTL, og dekker alt fra de grunnleggende konseptene til avanserte teknikker, slik at du kan bygge robuste og vedlikeholdbare React-applikasjoner.

Hvorfor velge React Testing Library?

Tradisjonelle testmetoder er ofte avhengige av implementeringsdetaljer, noe som gjør tester skjøre og utsatt for å brytes ved mindre kodeendringer. RTL, derimot, oppfordrer deg til å teste komponentene dine slik en bruker vil samhandle med dem, med fokus på hva brukeren ser og opplever. Denne tilnærmingen gir flere viktige fordeler:

Sette opp testmiljøet ditt

Før du kan begynne å bruke RTL, må du sette opp testmiljøet ditt. Dette innebærer vanligvis å installere de nødvendige avhengighetene og konfigurere testrammeverket ditt.

Forutsetninger

Installasjon

Installer følgende pakker ved hjelp av npm eller yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Eller, ved hjelp av yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Forklaring av pakker:

Konfigurasjon

Opprett en `babel.config.js`-fil i roten av prosjektet ditt med følgende innhold:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Oppdater `package.json`-filen din for å inkludere et testskript:

{
  "scripts": {
    "test": "jest"
  }
}

Opprett en `jest.config.js`-fil i roten av prosjektet ditt for å konfigurere Jest. En minimal konfigurasjon kan se slik ut:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Opprett en `src/setupTests.js`-fil med følgende innhold. Dette sikrer at Jest DOM-matchere er tilgjengelige i alle testene dine:

import '@testing-library/jest-dom/extend-expect';

Skrive din første test

La oss starte med et enkelt eksempel. Anta at du har en React-komponent som viser en hilsenmelding:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

La oss nå skrive en test for denne komponenten:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renders a greeting message', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/Hello, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

Forklaring:

For å kjøre testen, utfør følgende kommando i terminalen din:

npm test

Hvis alt er konfigurert riktig, skal testen bestå.

Vanlige RTL-spørringer

RTL gir forskjellige spørringsmetoder for å finne elementer i DOM. Disse spørringene er designet for å etterligne hvordan brukere samhandler med applikasjonen din.

`getByRole`

Denne spørringen finner et element etter sin ARIA-rolle. Det er en god praksis å bruke `getByRole` når det er mulig, da det fremmer tilgjengelighet og sikrer at testene dine er motstandsdyktige mot endringer i den underliggende DOM-strukturen.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Denne spørringen finner et element etter teksten i den tilhørende etiketten. Det er nyttig for å teste skjemafelt.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Denne spørringen finner et element etter plassholderteksten.

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Denne spørringen finner et bildeelement etter alt-teksten. Det er viktig å gi meningsfull alt-tekst for alle bilder for å sikre tilgjengelighet.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Denne spørringen finner et element etter tittelattributtet.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Denne spørringen finner et element etter visningsverdien. Dette er nyttig for å teste skjemaelementer med forhåndsutfylte verdier.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

`getAllBy*`-spørringer

I tillegg til `getBy*`-spørringene, tilbyr RTL også `getAllBy*`-spørringer, som returnerer en matrise med samsvarende elementer. Disse er nyttige når du trenger å hevde at flere elementer med de samme egenskapene er tilstede i DOM.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

`queryBy*`-spørringer

`queryBy*`-spørringene ligner på `getBy*`-spørringer, men de returnerer `null` hvis ingen samsvarende elementer blir funnet, i stedet for å kaste en feil. Dette er nyttig når du vil hevde at et element *ikke* er tilstede i DOM.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

`findBy*`-spørringer

`findBy*`-spørringene er asynkrone versjoner av `getBy*`-spørringene. De returnerer et Promise som løses når det samsvarende elementet blir funnet. Disse er nyttige for å teste asynkrone operasjoner, som å hente data fra et API.

// Simulerer en asynkron datahenting
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

Simulere brukerinteraksjoner

RTL gir `fireEvent`- og `userEvent`-APIene for å simulere brukerinteraksjoner, som å klikke på knapper, skrive i inndatafelter og sende inn skjemaer.

`fireEvent`

`fireEvent` lar deg programmatisk utløse DOM-hendelser. Det er et API på lavere nivå som gir deg finkornet kontroll over hendelsene som utløses.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('simulates a button click', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` er et API på høyere nivå som simulerer brukerinteraksjoner mer realistisk. Det håndterer detaljer som fokusstyring og hendelsesrekkefølge, noe som gjør testene dine mer robuste og mindre skjøre.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simulates typing in an input field', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

Teste asynkron kode

Mange React-applikasjoner involverer asynkrone operasjoner, som å hente data fra et API. RTL gir flere verktøy for å teste asynkron kode.

`waitFor`

`waitFor` lar deg vente til en betingelse blir sann før du gjør en påstand. Det er nyttig for å teste asynkrone operasjoner som tar litt tid å fullføre.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('waits for data to load', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

`findBy*`-spørringer

Som nevnt tidligere er `findBy*`-spørringene asynkrone og returnerer et Promise som løses når det samsvarende elementet blir funnet. Disse er nyttige for å teste asynkrone operasjoner som resulterer i endringer i DOM.

Teste Hooks

React Hooks er gjenbrukbare funksjoner som innkapsler stateful logikk. RTL gir `renderHook`-verktøyet fra `@testing-library/react-hooks` (som er foreldet til fordel for `@testing-library/react` direkte fra og med v17) for å teste egendefinerte Hooks i isolasjon.

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('increments the counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Forklaring:

Avanserte testteknikker

Når du har mestret det grunnleggende om RTL, kan du utforske mer avanserte testteknikker for å forbedre kvaliteten og vedlikeholdbarheten til testene dine.

Mocking av moduler

Noen ganger kan det hende du må mocke eksterne moduler eller avhengigheter for å isolere komponentene dine og kontrollere oppførselen deres under testing. Jest gir et kraftig mocking-API for dette formålet.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('fetches data from the API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Forklaring:

Kontekstleverandører

Hvis komponenten din er avhengig av en kontekstleverandør, må du pakke komponenten din inn i leverandøren under testing. Dette sikrer at komponenten har tilgang til kontekstverdiene.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('toggles the theme', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

Forklaring:

Testing med Router

Når du tester komponenter som bruker React Router, må du gi en mock Router-kontekst. Du kan oppnå dette ved å bruke `MemoryRouter`-komponenten fra `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('renders a link to the about page', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Forklaring:

Beste praksis for å skrive effektive tester

Her er noen anbefalte fremgangsmåter du bør følge når du skriver tester med RTL:

Konklusjon

React Testing Library er et kraftig verktøy for å skrive effektive, vedlikeholdbare og brukersentriske tester for dine React-applikasjoner. Ved å følge prinsippene og teknikkene som er beskrevet i denne veiledningen, kan du bygge robuste og pålitelige applikasjoner som oppfyller behovene til brukerne dine. Husk å fokusere på testing fra brukerens perspektiv, unngå å teste implementeringsdetaljer og skrive klare og konsise tester. Ved å omfavne RTL og ta i bruk beste praksis, kan du forbedre kvaliteten og vedlikeholdbarheten til React-prosjektene dine betydelig, uavhengig av hvor du befinner deg eller de spesifikke kravene til ditt globale publikum.

React Testing Library: En omfattende guide | MLOG