Mestr React Testing Library (RTL) med denne komplette guide. Lær at skrive effektive, vedligeholdelsesvenlige og brugercentrerede tests for dine React-applikationer, med fokus på bedste praksis og eksempler fra den virkelige verden.
React Testing Library: En Omfattende Guide
I nutidens hurtige webudviklingslandskab er det altafgørende at sikre kvaliteten og pålideligheden af dine React-applikationer. React Testing Library (RTL) er blevet en populær og effektiv løsning til at skrive tests, der fokuserer på brugerperspektivet. Denne guide giver en komplet oversigt over RTL, der dækker alt fra de grundlæggende koncepter til avancerede teknikker, og giver dig mulighed for at bygge robuste og vedligeholdelsesvenlige React-applikationer.
Hvorfor Vælge React Testing Library?
Traditionelle testtilgange er ofte afhængige af implementeringsdetaljer, hvilket gør tests skrøbelige og tilbøjelige til at gå i stykker ved mindre kodeændringer. RTL, derimod, opfordrer dig til at teste dine komponenter, som en bruger ville interagere med dem, med fokus på, hvad brugeren ser og oplever. Denne tilgang giver flere centrale fordele:
- Brugercentreret Testning: RTL fremmer skrivning af tests, der afspejler brugerens perspektiv, og sikrer, at din applikation fungerer som forventet fra slutbrugerens synspunkt.
- Reduceret Testskrøbelighed: Ved at undgå at teste implementeringsdetaljer er RTL-tests mindre tilbøjelige til at gå i stykker, når du refaktorerer din kode, hvilket fører til mere vedligeholdelsesvenlige og robuste tests.
- Forbedret Kodedesign: RTL opfordrer dig til at skrive komponenter, der er tilgængelige og lette at bruge, hvilket fører til et generelt bedre kodedesign.
- Fokus på Tilgængelighed: RTL gør det lettere at teste tilgængeligheden af dine komponenter og sikrer, at din applikation kan bruges af alle.
- Forenklet Testproces: RTL tilbyder en simpel og intuitiv API, der gør det lettere at skrive og vedligeholde tests.
Opsætning af Dit Testmiljø
Før du kan begynde at bruge RTL, skal du opsætte dit testmiljø. Dette involverer typisk installation af de nødvendige afhængigheder og konfiguration af dit test-framework.
Forudsætninger
- Node.js og npm (eller yarn): Sørg for, at du har Node.js og npm (eller yarn) installeret på dit system. Du kan downloade dem fra den officielle Node.js-hjemmeside.
- React-projekt: Du bør have et eksisterende React-projekt eller oprette et nyt ved hjælp af Create React App eller et lignende værktøj.
Installation
Installer følgende pakker ved hjælp af 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 hjælp af yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Forklaring af Pakker:
- @testing-library/react: Kernenbiblioteket til test af React-komponenter.
- @testing-library/jest-dom: Tilbyder brugerdefinerede Jest-matchere til at lave assertions om DOM-noder.
- Jest: Et populært JavaScript-test-framework.
- babel-jest: En Jest-transformer, der bruger Babel til at kompilere din kode.
- @babel/preset-env: En Babel-preset, der bestemmer de Babel-plugins og -presets, der er nødvendige for at understøtte dine målmiljøer.
- @babel/preset-react: En Babel-preset til React.
Konfiguration
Opret en `babel.config.js`-fil i roden af dit projekt med følgende indhold:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Opdater din `package.json`-fil for at inkludere et test-script:
{
"scripts": {
"test": "jest"
}
}
Opret en `jest.config.js`-fil i roden af dit projekt for at konfigurere Jest. En minimal konfiguration kan se sådan ud:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Opret en `src/setupTests.js`-fil med følgende indhold. Dette sikrer, at Jest DOM-matchere er tilgængelige i alle dine tests:
import '@testing-library/jest-dom/extend-expect';
Skriv Din Første Test
Lad os starte med et simpelt eksempel. Antag, at du har en React-komponent, der viser en hilsen:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Lad os nu skrive en test for denne komponent:
// 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 funktion renderer komponenten i DOM'en.
- `screen`: Dette objekt giver metoder til at forespørge i DOM'en.
- `getByText`: Denne metode finder et element ud fra dets tekstindhold. `/i`-flaget gør søgningen case-insensitiv.
- `expect`: Denne funktion bruges til at lave assertions om komponentens adfærd.
- `toBeInTheDocument`: Denne matcher asserter, at elementet er til stede i DOM'en.
For at køre testen, skal du udføre følgende kommando i din terminal:
npm test
Hvis alt er konfigureret korrekt, bør testen bestå.
Almindelige RTL Queries
RTL tilbyder forskellige query-metoder til at finde elementer i DOM'en. Disse queries er designet til at efterligne, hvordan brugere interagerer med din applikation.
`getByRole`
Denne query finder et element ud fra dets ARIA-rolle. Det er god praksis at bruge `getByRole`, når det er muligt, da det fremmer tilgængelighed og sikrer, at dine tests er modstandsdygtige over for ændringer i den underliggende DOM-struktur.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Denne query finder et element ud fra teksten på dets tilknyttede label. Det er nyttigt til at teste form-elementer.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Denne query finder et element ud fra dets placeholder-tekst.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Denne query finder et billedelement ud fra dets alt-tekst. Det er vigtigt at give meningsfuld alt-tekst til alle billeder for at sikre tilgængelighed.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Denne query finder et element ud fra dets title-attribut.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Denne query finder et element ud fra dets display-værdi. Dette er nyttigt til at teste form-input med forudfyldte værdier.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
`getAllBy*` Queries
Ud over `getBy*`-queries tilbyder RTL også `getAllBy*`-queries, som returnerer et array af matchende elementer. Disse er nyttige, når du skal assertere, at flere elementer med de samme egenskaber er til stede i DOM'en.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*` Queries
`queryBy*`-queries ligner `getBy*`-queries, men de returnerer `null`, hvis der ikke findes noget matchende element, i stedet for at kaste en fejl. Dette er nyttigt, når du vil assertere, at et element *ikke* er til stede i DOM'en.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
`findBy*` Queries
`findBy*`-queries er asynkrone versioner af `getBy*`-queries. De returnerer et Promise, der resolver, når det matchende element er fundet. Disse er nyttige til at teste asynkrone operationer, såsom hentning af data fra en API.
// Simulating an asynchronous data fetch
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();
});
Simulering af Brugerinteraktioner
RTL tilbyder `fireEvent`- og `userEvent`-API'erne til at simulere brugerinteraktioner, såsom at klikke på knapper, skrive i inputfelter og indsende formularer.
`fireEvent`
`fireEvent` giver dig mulighed for programmatisk at udløse DOM-events. Det er en lavere-niveau API, der giver dig finkornet kontrol over de events, der udlø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 en højere-niveau API, der simulerer brugerinteraktioner mere realistisk. Den håndterer detaljer som fokusstyring og event-rækkefølge, hvilket gør dine tests mere robuste og mindre skrøbelige.
<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!');
});
Testning af Asynkron Kode
Mange React-applikationer involverer asynkrone operationer, såsom at hente data fra en API. RTL tilbyder flere værktøjer til at teste asynkron kode.
`waitFor`
`waitFor` giver dig mulighed for at vente på, at en betingelse bliver sand, før du laver en assertion. Det er nyttigt til at teste asynkrone operationer, der tager lidt tid at fuldfø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*` Queries
Som nævnt tidligere, er `findBy*`-queries asynkrone og returnerer et Promise, der resolver, når det matchende element er fundet. Disse er nyttige til at teste asynkrone operationer, der resulterer i ændringer i DOM'en.
Testning af Hooks
React Hooks er genanvendelige funktioner, der indkapsler stateful logik. RTL tilbyder `renderHook`-værktøjet fra `@testing-library/react-hooks` (som er forældet til fordel for `@testing-library/react` direkte fra v17) til at teste brugerdefinerede Hooks i isolation.
// 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 funktion renderer Hook'en og returnerer et objekt, der indeholder resultatet af Hook'en.
- `act`: Denne funktion bruges til at ombryde enhver kode, der forårsager state-opdateringer. Dette sikrer, at React kan batche og behandle opdateringerne korrekt.
Avancerede Testteknikker
Når du har mestret det grundlæggende i RTL, kan du udforske mere avancerede testteknikker for at forbedre kvaliteten og vedligeholdelsen af dine tests.
Mocking af Moduler
Nogle gange kan det være nødvendigt at mocke eksterne moduler eller afhængigheder for at isolere dine komponenter og kontrollere deres adfærd under test. Jest tilbyder en kraftfuld mocking-API til dette formål.
// 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 linje mocker `dataService`-modulet.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Denne linje konfigurerer den mockede `fetchData`-funktion til at returnere et Promise, der resolver med de specificerede data.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Denne linje asserter, at den mockede `fetchData`-funktion blev kaldt én gang.
Context Providers
Hvis din komponent er afhængig af en Context Provider, skal du ombryde din komponent i provideren under test. Dette sikrer, at komponenten har adgang til context-værdierne.
// 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 ombryder `MyComponent` i `ThemeProvider` for at levere den nødvendige context under test.
Testning med Router
Når du tester komponenter, der bruger React Router, skal du levere en mock Router-context. Du kan opnå dette ved at bruge `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 ombryder `MyComponent` i `MemoryRouter` for at levere en mock Router-context.
- Vi asserter, at link-elementet har den korrekte `href`-attribut.
Bedste Praksis for at Skrive Effektive Tests
Her er nogle bedste praksisser, du kan følge, når du skriver tests med RTL:
- Fokuser på Brugerinteraktioner: Skriv tests, der simulerer, hvordan brugere interagerer med din applikation.
- Undgå at Teste Implementeringsdetaljer: Test ikke den interne funktion af dine komponenter. Fokuser i stedet på den observerbare adfærd.
- Skriv Klare og Præcise Tests: Gør dine tests lette at forstå og vedligeholde.
- Brug Meningsfulde Testnavne: Vælg testnavne, der præcist beskriver den adfærd, der testes.
- Hold Tests Isolerede: Undgå afhængigheder mellem tests. Hver test skal være uafhængig og selvstændig.
- Test Edge Cases: Test ikke kun den glade vej. Sørg også for at teste edge cases og fejltilstande.
- Skriv Tests, Før Du Koder: Overvej at bruge Test-Driven Development (TDD) til at skrive tests, før du skriver din kode.
- Følg "AAA"-Mønstret: Arrange, Act, Assert. Dette mønster hjælper med at strukturere dine tests og gøre dem mere læsbare.
- Hold dine tests hurtige: Langsomme tests kan afskrække udviklere fra at køre dem hyppigt. Optimer dine tests for hastighed ved at mocke netværksanmodninger og minimere mængden af DOM-manipulation.
- Brug beskrivende fejlmeddelelser: Når assertions fejler, skal fejlmeddelelserne give tilstrækkelig information til hurtigt at identificere årsagen til fejlen.
Konklusion
React Testing Library er et kraftfuldt værktøj til at skrive effektive, vedligeholdelsesvenlige og brugercentrerede tests for dine React-applikationer. Ved at følge principperne og teknikkerne beskrevet i denne guide, kan du bygge robuste og pålidelige applikationer, der imødekommer dine brugeres behov. Husk at fokusere på at teste fra brugerens perspektiv, undgå at teste implementeringsdetaljer, og skriv klare og præcise tests. Ved at omfavne RTL og anvende bedste praksis kan du markant forbedre kvaliteten og vedligeholdelsen af dine React-projekter, uanset din placering eller de specifikke krav fra dit globale publikum.