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:
- Brukersentrisk testing: RTL fremmer skriving av tester som gjenspeiler brukerens perspektiv, og sikrer at applikasjonen din fungerer som forventet fra sluttbrukerens synspunkt.
- Redusert testskjørhet: Ved å unngå å teste implementeringsdetaljer, er det mindre sannsynlig at RTL-tester brytes når du refaktorerer koden din, noe som fører til mer vedlikeholdbare og robuste tester.
- Forbedret kodedesign: RTL oppfordrer deg til å skrive komponenter som er tilgjengelige og enkle å bruke, noe som fører til bedre generell kodedesign.
- Fokus på tilgjengelighet: RTL gjør det lettere å teste tilgjengeligheten til komponentene dine, og sikrer at applikasjonen din er brukbar for alle.
- Forenklet testprosess: RTL gir et enkelt og intuitivt API, noe som gjør det lettere å skrive og vedlikeholde tester.
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
- Node.js og npm (eller yarn): Sørg for at du har Node.js og npm (eller yarn) installert på systemet ditt. Du kan laste dem ned fra den offisielle Node.js-nettsiden.
- React-prosjekt: Du bør ha et eksisterende React-prosjekt eller opprette et nytt ved hjelp av Create React App eller et lignende verktøy.
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:
- @testing-library/react: Kjernebiblioteket for testing av React-komponenter.
- @testing-library/jest-dom: Gir tilpassede Jest-matchere for å hevde om DOM-noder.
- Jest: Et populært JavaScript-testrammeverk.
- babel-jest: En Jest-transformator som bruker Babel til å kompilere koden din.
- @babel/preset-env: En Babel-forhåndsinnstilling som bestemmer Babel-plugins og -forhåndsinnstillinger som trengs for å støtte målmiljøene dine.
- @babel/preset-react: En Babel-forhåndsinnstilling for React.
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:
- `render`: Denne funksjonen gjengir komponenten i DOM.
- `screen`: Dette objektet gir metoder for å spørre DOM.
- `getByText`: Denne metoden finner et element etter tekstinnholdet. `/i`-flagget gjør søket case-insensitivt.
- `expect`: Denne funksjonen brukes til å gjøre påstander om komponentens oppførsel.
- `toBeInTheDocument`: Denne matcheren hevder at elementet er tilstede i DOM.
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:
- `renderHook`: Denne funksjonen gjengir Hook og returnerer et objekt som inneholder resultatet av Hook.
- `act`: Denne funksjonen brukes til å pakke inn kode som forårsaker tilstandsoppdateringer. Dette sikrer at React kan batche og behandle oppdateringene på riktig måte.
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:
- `jest.mock('../api/dataService')`: Denne linjen mocker `dataService`-modulen.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Denne linjen konfigurerer den mocked `fetchData`-funksjonen til å returnere et Promise som løses med de spesifiserte dataene.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Denne linjen hevder at den mocked `fetchData`-funksjonen ble kalt én gang.
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:
- Vi pakker `MyComponent` inn i `ThemeProvider` for å gi den nødvendige konteksten under testing.
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:
- Vi pakker `MyComponent` inn i `MemoryRouter` for å gi en mock Router-kontekst.
- Vi hevder at lenkeelementet har det riktige `href`-attributtet.
Beste praksis for å skrive effektive tester
Her er noen anbefalte fremgangsmåter du bør følge når du skriver tester med RTL:
- Fokus på brukerinteraksjoner: Skriv tester som simulerer hvordan brukere samhandler med applikasjonen din.
- Unngå å teste implementeringsdetaljer: Ikke test den interne funksjonen til komponentene dine. Fokuser i stedet på den observerbare oppførselen.
- Skriv klare og konsise tester: Gjør testene dine enkle å forstå og vedlikeholde.
- Bruk meningsfulle testnavn: Velg testnavn som nøyaktig beskriver oppførselen som testes.
- Hold tester isolert: Unngå avhengigheter mellom tester. Hver test skal være uavhengig og selvstendig.
- Test grensetilfeller: Ikke bare test den "lykkelige stien". Sørg for å teste grensetilfeller og feiltilstander også.
- Skriv tester før du koder: Vurder å bruke testdrevet utvikling (TDD) for å skrive tester før du skriver koden din.
- Følg "AAA"-mønsteret: Arrange, Act, Assert. Dette mønsteret hjelper deg med å strukturere testene dine og gjøre dem mer lesbare.
- Hold testene dine raske: Treghet i testene kan gjøre utviklere motvillige til å kjøre dem ofte. Optimaliser testene dine for fart ved å mocke nettverksforespørsler og minimere mengden DOM-manipulasjon.
- Bruk beskrivende feilmeldinger: Når påstander mislykkes, bør feilmeldingene gi tilstrekkelig informasjon til raskt å identifisere årsaken til feilen.
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.