Nederlands

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:

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

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:

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:

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:

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:

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:

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:

Best practices voor het schrijven van effectieve tests

Hier zijn enkele best practices om te volgen bij het schrijven van tests met RTL:

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.