Deutsch

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:

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

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:

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:

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:

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:

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:

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:

Best Practices für das Schreiben effektiver Tests

Hier sind einige Best Practices, die Sie beim Schreiben von Tests mit RTL befolgen sollten:

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.

React Testing Library: Ein umfassender Leitfaden | MLOG