Beheers React Testing Library (RTL) met deze complete gids. Leer hoe u effectieve, onderhoudbare en gebruikersgerichte tests schrijft voor uw React-applicaties, met de focus op best practices en praktijkvoorbeelden.
React Testing Library: Een Uitgebreide Gids
In het snelle webontwikkelingslandschap van vandaag is het waarborgen van de kwaliteit en betrouwbaarheid van uw React-applicaties van het grootste belang. React Testing Library (RTL) is uitgegroeid tot een populaire en effectieve oplossing voor het schrijven van tests die zich richten op het gebruikersperspectief. Deze gids biedt een compleet overzicht van RTL, van de fundamentele concepten tot geavanceerde technieken, zodat u robuuste en onderhoudbare React-applicaties kunt bouwen.
Waarom kiezen voor React Testing Library?
Traditionele testmethoden zijn vaak afhankelijk van implementatiedetails, waardoor tests breekbaar worden en snel falen bij kleine codewijzigingen. RTL moedigt u daarentegen aan om uw componenten te testen zoals een gebruiker ermee zou interageren, met de focus op wat de gebruiker ziet en ervaart. Deze aanpak biedt verschillende belangrijke voordelen:
- Gebruikersgericht testen: RTL bevordert het schrijven van tests die het perspectief van de gebruiker weerspiegelen, en zorgt ervoor dat uw applicatie functioneert zoals verwacht vanuit het oogpunt van de eindgebruiker.
- Minder breekbare tests: Door het vermijden van het testen van implementatiedetails, is de kans kleiner dat RTL-tests breken wanneer u uw code refactort, wat leidt tot meer onderhoudbare en robuuste tests.
- Verbeterd codeontwerp: RTL moedigt u aan om componenten te schrijven die toegankelijk en gemakkelijk te gebruiken zijn, wat leidt tot een beter algemeen codeontwerp.
- Focus op toegankelijkheid: RTL maakt het gemakkelijker om de toegankelijkheid van uw componenten te testen, zodat uw applicatie voor iedereen bruikbaar is.
- Vereenvoudigd testproces: RTL biedt een eenvoudige en intuïtieve API, waardoor het schrijven en onderhouden van tests gemakkelijker wordt.
Je testomgeving opzetten
Voordat u RTL kunt gaan gebruiken, moet u uw testomgeving opzetten. Dit omvat doorgaans het installeren van de benodigde afhankelijkheden en het configureren van uw testframework.
Vereisten
- Node.js en npm (of yarn): Zorg ervoor dat u Node.js en npm (of yarn) op uw systeem hebt geïnstalleerd. U kunt ze downloaden van de officiële Node.js-website.
- React-project: U dient een bestaand React-project te hebben of een nieuw project te maken met Create React App of een vergelijkbare tool.
Installatie
Installeer de volgende pakketten met npm of yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Of, met yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Uitleg van de pakketten:
- @testing-library/react: De kernbibliotheek voor het testen van React-componenten.
- @testing-library/jest-dom: Biedt aangepaste Jest-matchers voor asserts over DOM-nodes.
- Jest: Een populair JavaScript-testframework.
- babel-jest: Een Jest-transformer die Babel gebruikt om uw code te compileren.
- @babel/preset-env: Een Babel-preset die de benodigde Babel-plugins en presets bepaalt om uw doelomgevingen te ondersteunen.
- @babel/preset-react: Een Babel-preset voor React.
Configuratie
Maak een `babel.config.js`-bestand aan in de root van uw project met de volgende inhoud:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Werk uw `package.json`-bestand bij om een testscript op te nemen:
{
"scripts": {
"test": "jest"
}
}
Maak een `jest.config.js`-bestand aan in de root van uw project om Jest te configureren. Een minimale configuratie kan er als volgt uitzien:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
Maak een `src/setupTests.js`-bestand aan met de volgende inhoud. Dit zorgt ervoor dat de Jest DOM-matchers beschikbaar zijn in al uw tests:
import '@testing-library/jest-dom/extend-expect';
Je eerste test schrijven
Laten we beginnen met een eenvoudig voorbeeld. Stel dat u een React-component heeft dat een welkomstbericht weergeeft:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Laten we nu een test schrijven voor dit component:
// 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();
});
Uitleg:
- `render`: Deze functie rendert het component in de DOM.
- `screen`: Dit object biedt methoden om de DOM te bevragen.
- `getByText`: Deze methode vindt een element op basis van de tekstinhoud. De `/i`-vlag maakt de zoekopdracht hoofdletterongevoelig.
- `expect`: Deze functie wordt gebruikt om asserts te doen over het gedrag van het component.
- `toBeInTheDocument`: Deze matcher stelt vast dat het element aanwezig is in de DOM.
Voer het volgende commando uit in uw terminal om de test uit te voeren:
npm test
Als alles correct is geconfigureerd, zou de test moeten slagen.
Veelvoorkomende RTL-queries
RTL biedt verschillende query-methoden om elementen in de DOM te vinden. Deze queries zijn ontworpen om na te bootsen hoe gebruikers met uw applicatie interageren.
`getByRole`
Deze query vindt een element op basis van zijn ARIA-rol. Het is een goede gewoonte om `getByRole` waar mogelijk te gebruiken, omdat dit de toegankelijkheid bevordert en ervoor zorgt dat uw tests bestand zijn tegen wijzigingen in de onderliggende DOM-structuur.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Deze query vindt een element aan de hand van de tekst van het bijbehorende label. Dit is handig voor het testen van formulierelementen.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Deze query vindt een element op basis van de placeholder-tekst.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Deze query vindt een afbeeldingselement op basis van de alt-tekst. Het is belangrijk om voor alle afbeeldingen een betekenisvolle alt-tekst te voorzien om de toegankelijkheid te garanderen.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Deze query vindt een element op basis van zijn title-attribuut.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Deze query vindt een element op basis van de weergegeven waarde. Dit is handig voor het testen van formulierinvoer met vooraf ingevulde waarden.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
`getAllBy*`-queries
Naast de `getBy*`-queries biedt RTL ook `getAllBy*`-queries, die een array van overeenkomende elementen retourneren. Deze zijn handig wanneer u wilt vaststellen dat meerdere elementen met dezelfde kenmerken aanwezig zijn in de DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*`-queries
De `queryBy*`-queries zijn vergelijkbaar met `getBy*`-queries, maar ze retourneren `null` als er geen overeenkomend element wordt gevonden, in plaats van een fout te gooien. Dit is handig wanneer u wilt vaststellen dat een element *niet* aanwezig is in de DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
`findBy*`-queries
De `findBy*`-queries zijn asynchrone versies van de `getBy*`-queries. Ze retourneren een Promise die wordt vervuld wanneer het overeenkomende element wordt gevonden. Deze zijn nuttig voor het testen van asynchrone operaties, zoals het ophalen van gegevens van een 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();
});
Gebruikersinteracties simuleren
RTL biedt de `fireEvent`- en `userEvent`-API's voor het simuleren van gebruikersinteracties, zoals het klikken op knoppen, typen in invoervelden en het verzenden van formulieren.
`fireEvent`
`fireEvent` stelt u in staat om programmatisch DOM-events te triggeren. Het is een lager-niveau API die u fijnmazige controle geeft over de events die worden geactiveerd.
<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` is een hoger-niveau API die gebruikersinteracties realistischer simuleert. Het behandelt details zoals focusmanagement en de volgorde van events, waardoor uw tests robuuster en minder breekbaar worden.
<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!');
});
Asynchrone code testen
Veel React-applicaties omvatten asynchrone operaties, zoals het ophalen van gegevens van een API. RTL biedt verschillende tools voor het testen van asynchrone code.
`waitFor`
`waitFor` stelt u in staat te wachten tot een voorwaarde waar wordt voordat u een assertie doet. Het is handig voor het testen van asynchrone operaties die enige tijd nodig hebben om te voltooien.
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
Zoals eerder vermeld, zijn de `findBy*`-queries asynchroon en retourneren ze een Promise die wordt vervuld wanneer het overeenkomstige element wordt gevonden. Deze zijn nuttig voor het testen van asynchrone operaties die resulteren in wijzigingen in de DOM.
Hooks testen
React Hooks zijn herbruikbare functies die stateful logica inkapselen. RTL biedt de `renderHook`-utility van `@testing-library/react-hooks` (die verouderd is ten gunste van `@testing-library/react` zelf vanaf v17) voor het testen van custom Hooks in isolatie.
// 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);
});
Uitleg:
- `renderHook`: Deze functie rendert de Hook en retourneert een object dat het resultaat van de Hook bevat.
- `act`: Deze functie wordt gebruikt om code te omvatten die state-updates veroorzaakt. Dit zorgt ervoor dat React de updates correct kan batchen en verwerken.
Geavanceerde testtechnieken
Zodra u de basis van RTL onder de knie heeft, kunt u meer geavanceerde testtechnieken verkennen om de kwaliteit en onderhoudbaarheid van uw tests te verbeteren.
Modules mocken
Soms moet u externe modules of afhankelijkheden mocken om uw componenten te isoleren en hun gedrag tijdens het testen te controleren. Jest biedt hiervoor een krachtige mocking-API.
// 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);
});
Uitleg:
- `jest.mock('../api/dataService')`: Deze regel mockt de `dataService`-module.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Deze regel configureert de gemockte `fetchData`-functie om een Promise te retourneren die wordt vervuld met de opgegeven data.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Deze regel stelt vast dat de gemockte `fetchData`-functie één keer is aangeroepen.
Context Providers
Als uw component afhankelijk is van een Context Provider, moet u uw component tijdens het testen in de provider wrappen. Dit zorgt ervoor dat het component toegang heeft tot de contextwaarden.
// 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();
});
Uitleg:
- We wrappen `MyComponent` in `ThemeProvider` om de benodigde context te bieden tijdens het testen.
Testen met Router
Bij het testen van componenten die React Router gebruiken, moet u een mock Router-context bieden. U kunt dit bereiken met het `MemoryRouter`-component van `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');
});
Uitleg:
- We wrappen `MyComponent` in `MemoryRouter` om een mock Router-context te bieden.
- We stellen vast dat het link-element het juiste `href`-attribuut heeft.
Best practices voor het schrijven van effectieve tests
Hier zijn enkele best practices om te volgen bij het schrijven van tests met RTL:
- Focus op gebruikersinteracties: Schrijf tests die simuleren hoe gebruikers met uw applicatie omgaan.
- Vermijd het testen van implementatiedetails: Test niet de interne werking van uw componenten. Richt u in plaats daarvan op het waarneembare gedrag.
- Schrijf duidelijke en beknopte tests: Maak uw tests gemakkelijk te begrijpen en te onderhouden.
- Gebruik betekenisvolle testnamen: Kies testnamen die het geteste gedrag nauwkeurig beschrijven.
- Houd tests geïsoleerd: Vermijd afhankelijkheden tussen tests. Elke test moet onafhankelijk en op zichzelf staand zijn.
- Test randgevallen: Test niet alleen het 'happy path'. Zorg ervoor dat u ook randgevallen en foutsituaties test.
- Schrijf tests voordat je codeert: Overweeg Test-Driven Development (TDD) te gebruiken om tests te schrijven voordat u uw code schrijft.
- Volg het 'AAA'-patroon: Arrange, Act, Assert. Dit patroon helpt om uw tests te structureren en leesbaarder te maken.
- Houd uw tests snel: Langzame tests kunnen ontwikkelaars ontmoedigen om ze regelmatig uit te voeren. Optimaliseer uw tests voor snelheid door netwerkverzoeken te mocken en de hoeveelheid DOM-manipulatie te minimaliseren.
- Gebruik beschrijvende foutmeldingen: Wanneer asserts mislukken, moeten de foutmeldingen voldoende informatie geven om de oorzaak van de fout snel te identificeren.
Conclusie
React Testing Library is een krachtig hulpmiddel voor het schrijven van effectieve, onderhoudbare en gebruikersgerichte tests voor uw React-applicaties. Door de principes en technieken in deze gids te volgen, kunt u robuuste en betrouwbare applicaties bouwen die voldoen aan de behoeften van uw gebruikers. Onthoud dat u moet testen vanuit het perspectief van de gebruiker, het testen van implementatiedetails moet vermijden en duidelijke en beknopte tests moet schrijven. Door RTL te omarmen en best practices toe te passen, kunt u de kwaliteit en onderhoudbaarheid van uw React-projecten aanzienlijk verbeteren, ongeacht uw locatie of de specifieke eisen van uw wereldwijde publiek.