Meistern Sie die React Testing Library (RTL) mit diesem vollständigen Leitfaden. Lernen Sie, wie Sie effektive, wartbare und benutzerzentrierte Tests für Ihre React-Anwendungen schreiben, mit Fokus auf Best Practices und Praxisbeispiele.
React Testing Library: Ein umfassender Leitfaden
In der heutigen schnelllebigen Welt der Webentwicklung ist die Sicherstellung der Qualität und Zuverlässigkeit Ihrer React-Anwendungen von größter Bedeutung. Die React Testing Library (RTL) hat sich als eine beliebte und effektive Lösung für das Schreiben von Tests etabliert, die sich auf die Benutzerperspektive konzentrieren. Dieser Leitfaden bietet einen vollständigen Überblick über RTL, von den grundlegenden Konzepten bis hin zu fortgeschrittenen Techniken, um Sie zu befähigen, robuste und wartbare React-Anwendungen zu erstellen.
Warum die React Testing Library wählen?
Traditionelle Testansätze stützen sich oft auf Implementierungsdetails, was Tests brüchig und anfällig für Fehler bei geringfügigen Code-Änderungen macht. RTL hingegen ermutigt Sie, Ihre Komponenten so zu testen, wie ein Benutzer mit ihnen interagieren würde, und konzentriert sich auf das, was der Benutzer sieht und erlebt. Dieser Ansatz bietet mehrere entscheidende Vorteile:
- Benutzerzentriertes Testen: RTL fördert das Schreiben von Tests, die die Perspektive des Benutzers widerspiegeln und sicherstellen, dass Ihre Anwendung aus Sicht des Endbenutzers wie erwartet funktioniert.
- Reduzierte Test-Brüchigkeit: Durch die Vermeidung des Testens von Implementierungsdetails brechen RTL-Tests seltener, wenn Sie Ihren Code refaktorisieren, was zu wartbareren und robusteren Tests führt.
- Verbessertes Code-Design: RTL ermutigt Sie, Komponenten zu schreiben, die zugänglich und einfach zu bedienen sind, was zu einem insgesamt besseren Code-Design führt.
- Fokus auf Barrierefreiheit: RTL erleichtert das Testen der Barrierefreiheit Ihrer Komponenten und stellt sicher, dass Ihre Anwendung für alle nutzbar ist.
- Vereinfachter Testprozess: RTL bietet eine einfache und intuitive API, die das Schreiben und Warten von Tests erleichtert.
Einrichten Ihrer Testumgebung
Bevor Sie mit der Verwendung von RTL beginnen können, müssen Sie Ihre Testumgebung einrichten. Dies beinhaltet typischerweise die Installation der erforderlichen Abhängigkeiten und die Konfiguration Ihres Test-Frameworks.
Voraussetzungen
- Node.js und npm (oder yarn): Stellen Sie sicher, dass Sie Node.js und npm (oder yarn) auf Ihrem System installiert haben. Sie können sie von der offiziellen Node.js-Website herunterladen.
- React-Projekt: Sie sollten ein bestehendes React-Projekt haben oder ein neues mit Create React App oder einem ähnlichen Tool erstellen.
Installation
Installieren Sie die folgenden Pakete mit npm oder yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Oder mit yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Erläuterung der Pakete:
- @testing-library/react: Die Kernbibliothek zum Testen von React-Komponenten.
- @testing-library/jest-dom: Bietet benutzerdefinierte Jest-Matcher für Assertions über DOM-Knoten.
- Jest: Ein beliebtes JavaScript-Test-Framework.
- babel-jest: Ein Jest-Transformer, der Babel verwendet, um Ihren Code zu kompilieren.
- @babel/preset-env: Ein Babel-Preset, das die benötigten Babel-Plugins und -Presets zur Unterstützung Ihrer Zielumgebungen bestimmt.
- @babel/preset-react: Ein Babel-Preset für React.
Konfiguration
Erstellen Sie eine `babel.config.js`-Datei im Stammverzeichnis Ihres Projekts mit folgendem Inhalt:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Aktualisieren Sie Ihre `package.json`-Datei, um ein Test-Skript hinzuzufügen:
{
"scripts": {
"test": "jest"
}
}
Erstellen Sie eine `jest.config.js`-Datei im Stammverzeichnis Ihres Projekts, um Jest zu konfigurieren. Eine minimale Konfiguration könnte so aussehen:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Erstellen Sie eine `src/setupTests.js`-Datei mit folgendem Inhalt. Dies stellt sicher, dass die Jest-DOM-Matcher in all Ihren Tests verfügbar sind:
import '@testing-library/jest-dom/extend-expect';
Schreiben Ihres ersten Tests
Beginnen wir mit einem einfachen Beispiel. Angenommen, Sie haben eine React-Komponente, die eine Begrüßungsnachricht anzeigt:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hallo, {name}!</h1>;
}
export default Greeting;
Schreiben wir nun einen Test für diese Komponente:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('rendert eine Begrüßungsnachricht', () => {
render(<Greeting name="Welt" />);
const greetingElement = screen.getByText(/Hallo, Welt!/i);
expect(greetingElement).toBeInTheDocument();
});
Erläuterung:
- `render`: Diese Funktion rendert die Komponente in das DOM.
- `screen`: Dieses Objekt bietet Methoden zur Abfrage des DOM.
- `getByText`: Diese Methode findet ein Element anhand seines Textinhalts. Das `/i`-Flag macht die Suche von der Groß- und Kleinschreibung unabhängig.
- `expect`: Diese Funktion wird verwendet, um Assertions über das Verhalten der Komponente zu machen.
- `toBeInTheDocument`: Dieser Matcher stellt sicher, dass das Element im DOM vorhanden ist.
Um den Test auszuführen, führen Sie den folgenden Befehl in Ihrem Terminal aus:
npm test
Wenn alles korrekt konfiguriert ist, sollte der Test erfolgreich sein.
Häufige RTL-Queries
RTL bietet verschiedene Query-Methoden zum Auffinden von Elementen im DOM. Diese Queries sind so konzipiert, dass sie die Interaktion der Benutzer mit Ihrer Anwendung nachahmen.
`getByRole`
Diese Query findet ein Element anhand seiner ARIA-Rolle. Es ist eine gute Praxis, `getByRole` wann immer möglich zu verwenden, da dies die Barrierefreiheit fördert und sicherstellt, dass Ihre Tests widerstandsfähig gegenüber Änderungen in der zugrunde liegenden DOM-Struktur sind.
<button role="button">Klick mich</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Diese Query findet ein Element anhand des Textes seines zugehörigen Labels. Sie ist nützlich zum Testen von Formularelementen.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Diese Query findet ein Element anhand seines Platzhaltertextes.
<input type="text" placeholder="Geben Sie Ihre E-Mail ein" />
const emailInputElement = screen.getByPlaceholderText('Geben Sie Ihre E-Mail ein');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Diese Query findet ein Bildelement anhand seines Alternativtextes. Es ist wichtig, für alle Bilder einen aussagekräftigen Alternativtext bereitzustellen, um die Barrierefreiheit zu gewährleisten.
<img src="logo.png" alt="Firmenlogo" />
const logoImageElement = screen.getByAltText('Firmenlogo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Diese Query findet ein Element anhand seines `title`-Attributs.
<span title="Schließen">X</span>
const closeElement = screen.getByTitle('Schließen');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Diese Query findet ein Element anhand seines angezeigten Wertes. Dies ist nützlich zum Testen von Formulareingaben mit vorausgefüllten Werten.
<input type="text" value="Anfangswert" />
const inputElement = screen.getByDisplayValue('Anfangswert');
expect(inputElement).toBeInTheDocument();
`getAllBy*`-Queries
Zusätzlich zu den `getBy*`-Queries bietet RTL auch `getAllBy*`-Queries, die ein Array von passenden Elementen zurückgeben. Diese sind nützlich, wenn Sie sicherstellen müssen, dass mehrere Elemente mit denselben Eigenschaften im DOM vorhanden sind.
<li>Element 1</li>
<li>Element 2</li>
<li>Element 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*`-Queries
Die `queryBy*`-Queries ähneln den `getBy*`-Queries, geben jedoch `null` zurück, wenn kein passendes Element gefunden wird, anstatt einen Fehler auszulösen. Dies ist nützlich, wenn Sie sicherstellen möchten, dass ein Element *nicht* im DOM vorhanden ist.
const missingElement = screen.queryByText('Nicht existierender Text');
expect(missingElement).toBeNull();
`findBy*`-Queries
Die `findBy*`-Queries sind asynchrone Versionen der `getBy*`-Queries. Sie geben ein Promise zurück, das aufgelöst wird, wenn das passende Element gefunden wird. Diese sind nützlich zum Testen asynchroner Operationen, wie z.B. das Abrufen von Daten von einer API.
// Simulation eines asynchronen Datenabrufs
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Daten geladen!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('lädt Daten asynchron', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Daten geladen!');
expect(dataElement).toBeInTheDocument();
});
Simulieren von Benutzerinteraktionen
RTL bietet die `fireEvent`- und `userEvent`-APIs zur Simulation von Benutzerinteraktionen wie dem Klicken von Schaltflächen, dem Tippen in Eingabefeldern und dem Absenden von Formularen.
`fireEvent`
`fireEvent` ermöglicht es Ihnen, DOM-Ereignisse programmatisch auszulösen. Es ist eine Low-Level-API, die Ihnen eine feingranulare Kontrolle über die ausgelösten Ereignisse gibt.
<button onClick={() => alert('Button geklickt!')}>Klick mich</button>
import { fireEvent } from '@testing-library/react';
test('simuliert einen Button-Klick', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button geklickt!')}>Klick mich</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` ist eine High-Level-API, die Benutzerinteraktionen realistischer simuliert. Sie kümmert sich um Details wie Fokusmanagement und Ereignisreihenfolge, was Ihre Tests robuster und weniger brüchig macht.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simuliert das Tippen in einem Eingabefeld', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hallo, Welt!');
expect(inputElement).toHaveValue('Hallo, Welt!');
});
Testen von asynchronem Code
Viele React-Anwendungen beinhalten asynchrone Operationen, wie das Abrufen von Daten von einer API. RTL bietet mehrere Werkzeuge zum Testen von asynchronem Code.
`waitFor`
`waitFor` ermöglicht es Ihnen, auf das Eintreten einer Bedingung zu warten, bevor Sie eine Assertion machen. Es ist nützlich zum Testen asynchroner Operationen, die einige Zeit in Anspruch nehmen.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Daten geladen!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('wartet auf das Laden der Daten', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Daten geladen!'));
const dataElement = screen.getByText('Daten geladen!');
expect(dataElement).toBeInTheDocument();
});
`findBy*`-Queries
Wie bereits erwähnt, sind die `findBy*`-Queries asynchron und geben ein Promise zurück, das aufgelöst wird, wenn das passende Element gefunden wird. Diese sind nützlich zum Testen asynchroner Operationen, die zu Änderungen im DOM führen.
Testen von Hooks
React Hooks sind wiederverwendbare Funktionen, die zustandsbehaftete Logik kapseln. RTL bietet das `renderHook`-Dienstprogramm von `@testing-library/react-hooks` (das zugunsten von `@testing-library/react` direkt ab v17 veraltet ist) zum isolierten Testen von benutzerdefinierten Hooks.
// 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('erhöht den Zähler', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Erläuterung:
- `renderHook`: Diese Funktion rendert den Hook und gibt ein Objekt zurück, das das Ergebnis des Hooks enthält.
- `act`: Diese Funktion wird verwendet, um jeden Code zu umschließen, der Zustandsaktualisierungen verursacht. Dies stellt sicher, dass React die Aktualisierungen ordnungsgemäß bündeln und verarbeiten kann.
Fortgeschrittene Testtechniken
Sobald Sie die Grundlagen von RTL beherrschen, können Sie fortgeschrittenere Testtechniken erkunden, um die Qualität und Wartbarkeit Ihrer Tests zu verbessern.
Mocking von Modulen
Manchmal müssen Sie möglicherweise externe Module oder Abhängigkeiten mocken, um Ihre Komponenten zu isolieren und ihr Verhalten während des Testens zu kontrollieren. Jest bietet zu diesem Zweck eine leistungsstarke 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('ruft Daten von der API ab', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Gemockte Daten!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Gemockte Daten!'));
expect(screen.getByText('Gemockte Daten!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Erläuterung:
- `jest.mock('../api/dataService')`: Diese Zeile mockt das `dataService`-Modul.
- `dataService.fetchData.mockResolvedValue({ message: 'Gemockte Daten!' })`: Diese Zeile konfiguriert die gemockte `fetchData`-Funktion so, dass sie ein Promise zurückgibt, das mit den angegebenen Daten aufgelöst wird.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Diese Zeile stellt sicher, dass die gemockte `fetchData`-Funktion einmal aufgerufen wurde.
Context-Provider
Wenn Ihre Komponente auf einen Context-Provider angewiesen ist, müssen Sie Ihre Komponente während des Testens in den Provider einbetten. Dadurch wird sichergestellt, dass die Komponente Zugriff auf die Kontextwerte hat.
// 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>Aktuelles Theme: {theme}</p>
<button onClick={toggleTheme}>Theme wechseln</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('wechselt das Theme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Aktuelles Theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Theme wechseln/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Aktuelles Theme: dark/i)).toBeInTheDocument();
});
Erläuterung:
- Wir betten die `MyComponent` in den `ThemeProvider` ein, um den notwendigen Kontext während des Testens bereitzustellen.
Testen mit Router
Beim Testen von Komponenten, die React Router verwenden, müssen Sie einen Mock-Router-Kontext bereitstellen. Dies können Sie mit der `MemoryRouter`-Komponente aus `react-router-dom` erreichen.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Zur Über-uns-Seite</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('rendert einen Link zur Über-uns-Seite', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Zur Über-uns-Seite/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Erläuterung:
- Wir betten die `MyComponent` in `MemoryRouter` ein, um einen Mock-Router-Kontext bereitzustellen.
- Wir stellen sicher, dass das Link-Element das korrekte `href`-Attribut hat.
Best Practices für das Schreiben effektiver Tests
Hier sind einige Best Practices, die Sie beim Schreiben von Tests mit RTL befolgen sollten:
- Fokus auf Benutzerinteraktionen: Schreiben Sie Tests, die simulieren, wie Benutzer mit Ihrer Anwendung interagieren.
- Vermeiden Sie das Testen von Implementierungsdetails: Testen Sie nicht die interne Funktionsweise Ihrer Komponenten. Konzentrieren Sie sich stattdessen auf das beobachtbare Verhalten.
- Schreiben Sie klare und prägnante Tests: Machen Sie Ihre Tests leicht verständlich und wartbar.
- Verwenden Sie aussagekräftige Testnamen: Wählen Sie Testnamen, die das zu testende Verhalten genau beschreiben.
- Halten Sie Tests isoliert: Vermeiden Sie Abhängigkeiten zwischen Tests. Jeder Test sollte unabhängig und in sich geschlossen sein.
- Testen Sie Grenzfälle: Testen Sie nicht nur den „Happy Path“. Stellen Sie sicher, dass Sie auch Grenzfälle und Fehlerbedingungen testen.
- Schreiben Sie Tests, bevor Sie codieren: Erwägen Sie die Verwendung von Test-Driven Development (TDD), um Tests zu schreiben, bevor Sie Ihren Code schreiben.
- Folgen Sie dem „AAA“-Muster: Arrange, Act, Assert. Dieses Muster hilft, Ihre Tests zu strukturieren und lesbarer zu machen.
- Halten Sie Ihre Tests schnell: Langsame Tests können Entwickler davon abhalten, sie häufig auszuführen. Optimieren Sie Ihre Tests auf Geschwindigkeit, indem Sie Netzwerkanfragen mocken und die Menge der DOM-Manipulation minimieren.
- Verwenden Sie beschreibende Fehlermeldungen: Wenn Assertions fehlschlagen, sollten die Fehlermeldungen genügend Informationen liefern, um die Fehlerursache schnell zu identifizieren.
Fazit
Die React Testing Library ist ein leistungsstarkes Werkzeug zum Schreiben effektiver, wartbarer und benutzerzentrierter Tests für Ihre React-Anwendungen. Indem Sie die in diesem Leitfaden beschriebenen Prinzipien und Techniken befolgen, können Sie robuste und zuverlässige Anwendungen erstellen, die den Bedürfnissen Ihrer Benutzer entsprechen. Denken Sie daran, sich auf das Testen aus der Benutzerperspektive zu konzentrieren, das Testen von Implementierungsdetails zu vermeiden und klare und prägnante Tests zu schreiben. Durch die Anwendung von RTL und die Übernahme von Best Practices können Sie die Qualität und Wartbarkeit Ihrer React-Projekte erheblich verbessern, unabhängig von Ihrem Standort oder den spezifischen Anforderungen Ihres globalen Publikums.