als Jest für React 2026
Jest war jahrelang der Standard für React-Tests – langsamer Start, aufwendige Transformer-Konfiguration und wachsende Inkompatibilitäten mit ESM. Vitest teilt Vites Konfiguration und Transformer, startet in Millisekunden, bietet eine Jest-kompatible API und integriert sich nahtlos in Vite-basierte React-Projekte.
Inhaltsverzeichnis
- 1. Das Problem mit Jest in Vite-Projekten
- 2. Warum Vitest schneller ist
- 3. Vitest einrichten: Installation und Konfiguration
- 4. React Testing Library integrieren
- 5. Erste Komponenten-Tests schreiben
- 6. Mocking mit vi.mock und vi.fn
- 7. Async-Tests und Server-Action-Mocking
- 8. Coverage und CI-Integration
- 9. Vitest vs. Jest: direkter Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit Jest in Vite-Projekten
Jest wurde für CommonJS entwickelt und hat eine komplexe Beziehung zu modernen ESM-Paketen. In Vite-Projekten, die native ES Module verwenden, entsteht ein strukturelles Problem: Jest verwendet seinen eigenen Transformer (Babel oder ts-jest), der unabhängig von Vites Konfiguration läuft. Das bedeutet, dass man zwei separate Transformer-Konfigurationen pflegen muss – eine für Vite und eine für Jest. Wenn ein Plugin in Vite die Modulauflösung anpasst (Pfad-Aliase, SVG als Komponenten, CSS Modules), muss diese Konfiguration manuell in Jest repliziert werden.
Das praktische Resultat: Jest-Konfiguration in Vite-Projekten ist oft mehr Wartungsaufwand als die Tests selbst. Pfad-Aliase werden im Test nicht aufgelöst, weil moduleNameMapper in der jest.config.ts nicht mit resolve.alias in vite.config.ts synchronisiert ist. ESM-Pakete erfordern transformIgnorePatterns-Ausnahmen. JSX in TypeScript braucht separates ts-jest-Setup. Der Watch-Modus ist langsam, weil Jest bei jeder Änderung mehr Module neu transformiert als nötig. Vitest löst all das durch einen einzigen Design-Entscheid: Es läuft im Kontext von Vite und teilt dessen Konfiguration.
2. Warum Vitest schneller ist
Vitest teilt Vites Transform-Pipeline. Das bedeutet: Dieselbe Transformer-Konfiguration, die im Dev-Server und Production-Build aktiv ist, ist auch in Tests aktiv – ohne Duplizierung. Pfad-Aliase aus vite.config.ts funktionieren sofort in Tests. SVG-Imports, CSS-Module und andere Vite-Plugins sind automatisch in der Test-Umgebung verfügbar. Es gibt keine separate Jest-Konfiguration, die synchron gehalten werden muss.
Die Performance-Vorteile kommen aus mehreren Quellen: Vitest nutzt Vites vorhandenene Modul-Cache. Im Watch-Modus werden nur die Module neu transformiert, die sich tatsächlich geändert haben – und nur die Tests neu ausgeführt, die von den geänderten Modulen abhängen. Das Ergebnis: Watch-Mode-Reaktionen in unter 100 Millisekunden statt mehrerer Sekunden. Für den initialen Test-Run ist Vitest durch parallele Ausführung in Worker-Threads ebenfalls deutlich schneller als Jest mit seiner synchronen Transformer-Pipeline. Auf großen Codebasen mit hunderten von Testdateien sind die Unterschiede drastisch.
// vitest.config.ts — or configure directly in vite.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react-swc';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
// Aliases work identically in tests — no duplication needed
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
},
},
test: {
environment: 'jsdom', // DOM environment for React components
globals: true, // no need to import describe/it/expect
setupFiles: ['./src/test/setup.ts'], // global test setup
css: true, // process CSS imports in tests
coverage: {
provider: 'v8', // fast V8 coverage (alternative: istanbul)
reporter: ['text', 'lcov', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.d.ts', 'src/test/**'],
},
},
});
3. Vitest einrichten: Installation und Konfiguration
Die Installation von Vitest in einem bestehenden Vite-React-Projekt ist minimal: npm install -D vitest @vitest/coverage-v8 jsdom. Dazu kommen @testing-library/react und @testing-library/user-event für Komponenten-Tests sowie @testing-library/jest-dom für erweiterte DOM-Matcher. Die Konfiguration kann entweder direkt in vite.config.ts unter dem test-Schlüssel oder in einer separaten vitest.config.ts erfolgen – letztere ist empfehlenswert, um Vite- und Test-Konfiguration getrennt zu halten.
Die Setup-Datei src/test/setup.ts initialisiert die globalen Test-Utilities. Das wichtigste: @testing-library/jest-dom importieren, damit Matcher wie toBeInTheDocument(), toHaveValue() und toBeDisabled() verfügbar sind. Mit globals: true in der Vitest-Konfiguration sind describe, it, expect, beforeEach und afterEach ohne Import nutzbar – exakt wie in Jest. Diese Einstellung macht die Migration von Jest zu Vitest in vielen Fällen zu einem reinen Konfigurationswechsel, ohne Testcode anfassen zu müssen.
4. React Testing Library integrieren
React Testing Library (RTL) und Vitest sind eine natürliche Kombination. RTL testet React-Komponenten, wie ein Nutzer sie sieht: durch Interaktion mit gerenderten DOM-Elementen, nicht durch Zugriff auf interne Komponentenzustände. Vitest liefert die Test-Runner-Infrastruktur, RTL die Test-Utilities. Die Integration erfordert die jsdom-Umgebung, die einen Browser-ähnlichen DOM im Node.js-Kontext simuliert.
Ein wichtiger Unterschied zu Jest: In React 18 mit Vitest muss man sicherstellen, dass act-Warnungen korrekt behandelt werden. Das userEvent-Package aus @testing-library/user-event ist dem älteren fireEvent vorzuziehen, da es die Benutzerinteraktion realistischer simuliert – inkl. Focus-Management, Tastaturnavigation und sequenzielle Events. In Vitest wird userEvent.setup() einmal pro Testblock aufgerufen, was einen isolierten User-Event-Kontext pro Test sicherstellt.
// src/test/setup.ts — global test setup file
import '@testing-library/jest-dom'; // extends expect with DOM matchers
// Clean up after each test — prevents memory leaks
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(cleanup);
// ---
// Example component test — LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LoginForm } from '@components/LoginForm'; // alias works natively
describe('LoginForm', () => {
const user = userEvent.setup(); // one user-event instance per describe block
it('shows error when submitting empty form', async () => {
render(<LoginForm onSubmit={vi.fn()} />);
const submitButton = screen.getByRole('button', { name: /anmelden/i });
await user.click(submitButton);
expect(screen.getByText(/email ist pflichtfeld/i)).toBeInTheDocument();
});
it('calls onSubmit with credentials on valid input', async () => {
const mockSubmit = vi.fn().mockResolvedValue({ success: true });
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@mironsoft.de');
await user.type(screen.getByLabelText(/passwort/i), 'supersecret123');
await user.click(screen.getByRole('button', { name: /anmelden/i }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@mironsoft.de',
password: 'supersecret123',
});
});
});
it('disables submit button while pending', async () => {
// Simulate slow async submit
const mockSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 500)));
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@mironsoft.de');
await user.type(screen.getByLabelText(/passwort/i), 'pass');
await user.click(screen.getByRole('button', { name: /anmelden/i }));
expect(screen.getByRole('button', { name: /anmelden/i })).toBeDisabled();
});
});
5. Erste Komponenten-Tests schreiben
Der erste Schritt beim Schreiben von Komponenten-Tests mit Vitest und RTL ist die Frage: Was testet dieser Test aus Nutzersicht? RTL fördert bewusst Tests, die auf sichtbaren Elementen basieren – Texte, Labels, Rollen und Platzhalter – statt auf internen Implementierungsdetails wie State-Variablen oder Komponentenstruktur. Ein Test, der auf document.querySelector('.my-button') basiert, ist fragil. Ein Test, der auf screen.getByRole('button', { name: /absenden/i }) basiert, überlebt Refactorings problemlos.
Die Query-Hierarchie in RTL ist wichtig: getByRole ist die bevorzugte Methode, weil sie gleichzeitig Accessibility überprüft. getByLabelText funktioniert nur, wenn Inputs korrekt mit Labels verknüpft sind. getByText ist nützlich für sichtbare Texte. getByTestId mit data-testid-Attributen ist der letzte Ausweg und sollte vermieden werden. Für asynchrone Updates verwendet man findBy*-Varianten (geben ein Promise zurück) oder waitFor() für komplexere Bedingungen. Vitest hat dafür vollständige jsdom-Unterstützung ohne zusätzliche Konfiguration.
6. Mocking mit vi.mock und vi.fn
Vitest verwendet vi als Äquivalent zu Jests jest-Namespace. Die API ist fast identisch: vi.fn() erstellt Mock-Funktionen, vi.mock('modulpfad') ersetzt ein ganzes Modul durch einen Mock, vi.spyOn(obj, 'method') überwacht Methoden-Aufrufe. Der wichtigste Unterschied: Vitest benutzt native ESM-Module, was bedeutet, dass vi.mock() hoisted wird – exakt wie in Jest, aber mit nativem ESM-Support statt Babel-Transforms.
Ein besonders nützliches Vitest-Feature für React ist das Mocking von Hooks und Service-Funktionen. Wenn eine Komponente einen API-Aufruf via Custom Hook macht, kann der Hook komplett gemockt werden, ohne die HTTP-Ebene zu berühren. Das Muster: vi.mock('../hooks/useApi', () => ({ useApi: vi.fn() })) im Testfile, dann vi.mocked(useApi).mockReturnValue({ data: mockData }) im Testfall. Für Cleanup zwischen Tests sorgt vi.clearAllMocks() oder vi.resetAllMocks() in beforeEach.
// Mocking API calls and modules in Vitest
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProductList } from '@components/ProductList';
// Mock the API module — hoisted automatically like Jest
vi.mock('@/api/products', () => ({
fetchProducts: vi.fn(),
}));
// Import after mock declaration to get the mocked version
import { fetchProducts } from '@/api/products';
const mockProducts = [
{ id: '1', name: 'Laptop Pro', price: 1299 },
{ id: '2', name: 'Maus Ergonomisch', price: 89 },
];
describe('ProductList', () => {
beforeEach(() => {
vi.clearAllMocks(); // reset between tests
});
it('renders products after successful fetch', async () => {
vi.mocked(fetchProducts).mockResolvedValue(mockProducts);
render(<ProductList />);
// Wait for async data to appear
await waitFor(() => {
expect(screen.getByText('Laptop Pro')).toBeInTheDocument();
});
expect(screen.getByText('Maus Ergonomisch')).toBeInTheDocument();
expect(screen.getByText('1.299 €')).toBeInTheDocument();
});
it('shows error state when fetch fails', async () => {
vi.mocked(fetchProducts).mockRejectedValue(new Error('Network error'));
render(<ProductList />);
await waitFor(() => {
expect(screen.getByText(/fehler beim laden/i)).toBeInTheDocument();
});
});
it('shows loading state initially', () => {
vi.mocked(fetchProducts).mockImplementation(() => new Promise(() => {})); // never resolves
render(<ProductList />);
expect(screen.getByRole('status', { name: /lädt/i })).toBeInTheDocument();
});
});
7. Async-Tests und Server-Action-Mocking
Asynchrone Tests sind in Vitest direkt aus der Box gut unterstützt. Das Muster für Tests, die auf asynchrone State-Updates warten: await waitFor(() => expect(...)) für einzelne Assertions oder await findByText('...') für DOM-Queries. waitFor pollt die Bedingung bis zum Timeout (Standard: 1000 ms) und schlägt fehl, wenn die Bedingung nicht eintritt. Das verhindert Test-Flakiness durch zu kurze Timeouts, die in anderen Test-Frameworks häufig mit expliziten Sleeps "gelöst" werden.
Für React 19 Server Actions im Test-Kontext ist das Mocking besonders relevant. Server Actions sind in Tests einfach normale async Funktionen – sie haben keine spezielle Server-Semantik im Client-Test-Kontext. vi.mock('./actions', () => ({ addTodoAction: vi.fn().mockResolvedValue({ id: '1', text: 'Todo' }) })) reicht aus, um Server-Action-Aufrufe in Komponenten-Tests zu kontrollieren. Das macht Vitest besonders gut für das Testen von Komponenten mit useOptimistic und useActionState, da alle asynchronen Aufrufe vollständig kontrollierbar sind.
8. Coverage und CI-Integration
Coverage in Vitest wird mit dem --coverage-Flag aktiviert. Zwei Provider stehen zur Verfügung: V8 (Googles eingebauter Coverage-Mechanismus, schneller, weniger präzise) und Istanbul (der Jest-Standard, etwas langsamer, branchen-genaue Berichte). Für die meisten Projekte ist V8 die richtige Wahl – es ist in Node.js eingebaut, braucht kein Instrumenting der Quell-Dateien und ist deutlich schneller. Istanbul ist sinnvoll, wenn sehr präzise Branch-Coverage-Reports für Audits benötigt werden.
In der CI-Pipeline ist das empfohlene Setup: vitest run --coverage (kein Watch-Modus in CI), Coverage-Reports als Artefakte speichern, und ein Coverage-Threshold setzen, das den Build scheitern lässt, wenn die Coverage-Werte fallen. Mit coverage.thresholds in der Vitest-Konfiguration lassen sich Minimumwerte für Zeilen, Funktionen, Branches und Statements festlegen. LCOV-Reports können in Coverage-Tools wie Codecov oder SonarQube hochgeladen werden. Die Integration in GitHub Actions ist mit zwei Zeilen erledigt.
| Eigenschaft | Jest (in Vite-Projekten) | Vitest |
|---|---|---|
| Vite-Konfiguration | Manuell duplizieren | Automatisch geteilt |
| Watch-Mode Reaktion | 2–10 Sek. | < 100 ms |
| Pfad-Aliase | moduleNameMapper manuell | Automatisch aus vite.config |
| ESM-Support | Fragil, transformIgnorePatterns | Nativ |
| TypeScript-Setup | ts-jest oder babel-jest | Out-of-the-box mit SWC |
10. Zusammenfassung
Vitest ist der konsequente Schritt in der Vite-Ökosystem-Strategie: Dieselbe Konfiguration, derselbe Transformer, dieselbe Modulauflösung für Entwicklung, Build und Tests. Das eliminiert die Hauptprobleme, die Jest in modernen Vite-Projekten verursacht: duplizierte Konfiguration, ESM-Inkompatibilitäten und langsame Watch-Mode-Zyklen. Die API-Kompatibilität mit Jest macht die Migration in den meisten Projekten zum reinen Konfigurationswechsel ohne Testcode-Anpassungen.
Die wichtigsten Entscheidungspunkte für das Test-Setup: jsdom als Environment für Komponenten-Tests, globals: true für Jest-kompatible Schreibweise, V8 als Coverage-Provider für maximale Geschwindigkeit, und userEvent statt fireEvent für realistische Interaktionssimulation. Für neue Projekte mit Vite ist Vitest ab 2026 die erste Wahl ohne Einschränkung. Für bestehende Jest-Projekte lohnt die Migration besonders dann, wenn die Jest-Konfiguration wächst und die Watch-Mode-Zyklen die Entwicklerproduktivität spürbar bremsen.
Vitest für React — Das Wichtigste auf einen Blick
Vite-Konfiguration geteilt
Pfad-Aliase, Plugins und Transformer aus vite.config.ts funktionieren automatisch in Tests – keine Duplizierung in jest.config.ts.
Watch-Mode unter 100 ms
Vitest kennt den Modul-Graphen und führt nur betroffene Tests neu aus. Reaktive Test-Zyklen während der Entwicklung – kein Warten.
Jest-kompatible API
describe, it, expect, vi.fn(), vi.mock() – fast identisch zu Jest. Migration oft ohne Testcode-Änderungen, nur Konfigurationswechsel.
RTL-Integration
React Testing Library + jsdom + @testing-library/jest-dom. userEvent.setup() pro Testblock für isolierte, realistische Interaktionssimulation.