Testing mit Vitest und React Testing Library in React
Testing mit Vitest und React Testing Library
~17 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Bisher haben wir jede Änderung manuell im Browser geprüft – funktioniert, wird aber bei wachsendem Projekt schnell unpraktikabel: bei JEDER Änderung müssten Sie ALLE bisherigen Features erneut von Hand durchklicken. Automatisierte Tests übernehmen genau das.
Vitest und React Testing Library installieren
Vitest ist ein Test-Runner, der speziell für Vite-Projekte gebaut ist (nutzt dieselbe Konfiguration, dieselbe Geschwindigkeit) – das direkte, moderne Äquivalent zum älteren Jest. React Testing Library ("RTL") ist die Bibliothek, mit der man Komponenten so testet, WIE ein echter Nutzer mit ihnen interagieren würde (Klicks, Texteingabe, sichtbarer Text), statt interne Implementierungsdetails zu prüfen.
npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomvite.config.js um Test-Konfiguration erweitern
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // simuliert einen Browser in Node.js, kein echter Browser nötig
globals: true, // erlaubt describe/it/expect ohne Import in jeder Testdatei
setupFiles: './src/test/setup.js',
},
});import '@testing-library/jest-dom';
// erweitert expect() um Matcher wie .toBeInTheDocument(), .toHaveTextContent() usw.Der erste Test: eine reine Funktion
Wir beginnen mit dem EINFACHSTEN Fall: bmiKategorie() aus der "React Native Referenz"-Serie kennen Sie bereits als Muster – eine reine Funktion (keine Komponente, kein State) ist am leichtesten zu testen. Unser Projekt hat kein BMI-Beispiel, aber filterItemsSlowlys Filterlogik aus SearchDemoPage (Kapitel 33) ist strukturell ähnlich. Testen wir stattdessen etwas aus unserem echten Projekt: die Gesamtsumme-Berechnung aus CartPage, ausgelagert als eigene, testbare Funktion:
export function berechneGesamtsumme(items) {
return items.reduce((summe, item) => summe + item.price * item.quantity, 0);
}import { describe, it, expect } from 'vitest';
import { berechneGesamtsumme } from './cartTotal';
describe('berechneGesamtsumme', () => {
it('gibt 0 für einen leeren Warenkorb zurück', () => {
expect(berechneGesamtsumme([])).toBe(0);
});
it('berechnet die Summe für einen einzelnen Artikel', () => {
const items = [{ price: 10, quantity: 2 }];
expect(berechneGesamtsumme(items)).toBe(20);
});
it('summiert mehrere unterschiedliche Artikel korrekt', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 },
];
expect(berechneGesamtsumme(items)).toBe(35);
});
});describe gruppiert zusammengehörige Tests, it (Alias: test) beschreibt EINEN konkreten Testfall in Alltagssprache, expect(...).toBe(...) ist die Behauptung selbst. Fügen Sie ein Skript in package.json hinzu: "test": "vitest", dann npm test ausführen – Vitest überwacht Dateien automatisch und läuft bei jeder Änderung erneut.
Eine Komponente testen: ProductCard
Komponenten-Tests prüfen NICHT internen State direkt, sondern SICHTBARES Verhalten – GENAU wie ein Nutzer die App erlebt. RTL's render() rendert die Komponente in eine virtuelle DOM-Umgebung, screen findet Elemente darin:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ProductCard from './ProductCard';
describe('ProductCard', () => {
const defaultProps = {
name: 'Wanderschuhe',
price: 89.99,
imageUrl: 'https://example.com/schuhe.jpg',
onSelect: vi.fn(),
onAddToCart: vi.fn(),
};
it('zeigt Name und Preis an', () => {
render(<ProductCard {...defaultProps} />);
expect(screen.getByText('Wanderschuhe')).toBeInTheDocument();
expect(screen.getByText('$89.99')).toBeInTheDocument();
});
it('ruft onAddToCart auf, wenn der Warenkorb-Button geklickt wird', async () => {
const user = userEvent.setup();
render(<ProductCard {...defaultProps} />);
await user.click(screen.getByText('In den Warenkorb'));
expect(defaultProps.onAddToCart).toHaveBeenCalledOnce();
expect(defaultProps.onSelect).not.toHaveBeenCalled();
});
it('schaltet den Favoriten-Status beim Klick auf das Herz um', async () => {
const user = userEvent.setup();
render(<ProductCard {...defaultProps} />);
expect(screen.getByText('♡')).toBeInTheDocument();
await user.click(screen.getByText('♡'));
expect(screen.getByText('♥')).toBeInTheDocument();
});
});Die Details, die diesen Test gut machen
vi.fn()erzeugt eine "Mock-Funktion" – eine Test-Attrappe, die aufgezeichnet wird, aber selbst nichts tut.toHaveBeenCalledOnce()prüft, dass sie GENAU einmal aufgerufen wurde.screen.getByText(...)sucht nach SICHTBAREM Text – genau das, was ein echter Nutzer sehen würde, nicht nach internen Variablennamen oder CSS-Klassen.expect(defaultProps.onSelect).not.toHaveBeenCalled()im "Add to Cart"-Test verifiziert INDIREKT, dassevent.stopPropagation()aus Kapitel 29 tatsächlich funktioniert – GENAU der Bug, denstopPropagation()verhindern soll, würde diesen Test fehlschlagen lassen.userEvent.setup()statt des älterenfireEventsimuliert ECHTE Nutzerinteraktion realistischer (inklusive Zwischenschritten wie Fokussieren vor dem Klicken).
Achtung: Testen Sie NICHT isFavorite als internen State direkt (z. B. über einen Komponenten-Instanz-Zugriff) – RTL bietet das absichtlich NICHT an. Der Test prüft stattdessen das SICHTBARE Ergebnis (♡ wird zu ♥) – bleibt der Test auch dann gültig, wenn Sie die interne Implementierung später ändern (z. B. isFavorite in einen Zustand-Store auslagern), solange das sichtbare Verhalten gleich bleibt.
Tipp: Faustregel der React Testing Library, oft zitiert: "The more your tests resemble the way your software is used, the more confidence they can give you." Tests, die Implementierungsdetails prüfen, brechen bei jedem Refactoring, selbst wenn das SICHTBARE Verhalten unverändert bleibt – genau das Gegenteil von dem, was gute Tests leisten sollen.