Unit Testing ist kein Nice-to-have mehr — es ist die Grundlage für jede AI-augmentierte Codebasis. Vitest hat sich 2026 als klarer Standard für TypeScript-Projekte durchgesetzt: native Vite-Integration, blitzschnelle HMR-basierte Testläufe und vollständige Jest-Kompatibilität. Claude Code generiert Vitest-Tests in Sekunden — mit korrekten Imports, echtem Mocking und Coverage-Konfiguration direkt aus dem Kontext.
| Feature | Vitest | Jest |
|---|---|---|
| ESM-Support | ✅ nativ | ⚠️ Transform nötig |
| TypeScript | ✅ zero config | ⚠️ ts-jest / babel |
| Vite-Integration | ✅ shared config | ❌ separat |
| Watch-Modus | ✅ HMR-basiert | ⚠️ langsamer |
| UI-Mode | ✅ eingebaut | ❌ nein |
| Coverage | ✅ v8 / istanbul | ✅ istanbul |
1. Vitest Setup — vite.config.ts test-Block
Der größte Vorteil von Vitest: eine einzige Config-Datei für Build und Tests.
Der test-Block in vite.config.ts konfiguriert alles — kein separates
jest.config.js mehr.
Installation
# Vitest und Test-Utilities installieren npm install -D vitest @vitest/ui @vitest/coverage-v8 npm install -D @testing-library/react jsdom happy-dom # Für React-Projekte zusätzlich npm install -D @testing-library/user-event @testing-library/jest-dom
vite.config.ts — vollständig
/// <reference types="vitest" /> import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { // Globals: describe/it/expect ohne Import verfügbar globals: true, // Environment: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' environment: 'jsdom', // Setup-Datei — läuft vor jedem Test setupFiles: ['./src/test/setup.ts'], // Welche Dateien als Tests erkannt werden include: ['**/*.{test,spec}.{ts,tsx}'], exclude: ['node_modules', 'dist', '**/*.e2e.*'], // Coverage-Konfiguration (optional hier, oder in vitest.config.ts) coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], include: ['src/**/*.{ts,tsx}'], exclude: ['src/test/**', '**/*.d.ts'], thresholds: { lines: 80, functions: 80, branches: 75, }, }, // Parallele Worker-Prozesse pool: 'threads', poolOptions: { threads: { singleThread: false } }, }, })
Setup-Datei (src/test/setup.ts)
import '@testing-library/jest-dom' import { afterEach } from 'vitest' import { cleanup } from '@testing-library/react' // Automatisch DOM nach jedem Test aufräumen afterEach(() => { cleanup() })
package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:bench": "vitest bench"
}
}
2. Tests schreiben — describe, it, expect
Vitest ist Jest-kompatibel — wer Jest kennt, kann sofort loslegen. Claude Code versteht beide APIs und generiert idiomatische Vitest-Tests mit korrekten Matchers.
Grundstruktur
import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { formatCurrency, calculateDiscount } from '../utils/pricing' describe('Pricing Utils', () => { describe('formatCurrency', () => { it('formatiert EUR korrekt', () => { expect(formatCurrency(1234.5, 'EUR')).toBe('1.234,50 €') }) it('behandelt Null-Werte', () => { expect(formatCurrency(0)).toBe('0,00 €') }) it('wirft bei negativem Betrag', () => { expect(() => formatCurrency(-10)).toThrow('Betrag darf nicht negativ sein') }) }) describe('calculateDiscount', () => { it.each([ [100, 10, 90], [200, 25, 150], [50, 0, 50], ])('%d€ mit %d%% Rabatt → %d€', (price, pct, expected) => { expect(calculateDiscount(price, pct)).toBe(expected) }) }) })
Die wichtigsten expect-Matchers
| Matcher | Verwendung |
|---|---|
.toBe(val) | Strikte Gleichheit (===) |
.toEqual(obj) | Tiefe Gleichheit (Objekte/Arrays) |
.toStrictEqual(obj) | Wie toEqual + undefined-Properties |
.toContain(item) | Array/String enthält Element |
.toHaveLength(n) | Array/String Länge |
.toMatchObject(obj) | Objekt enthält diese Keys |
.toThrow(msg?) | Funktion wirft Error |
.toMatchSnapshot() | Snapshot-Vergleich |
.toMatchInlineSnapshot() | Snapshot inline im Code |
.toBeGreaterThan(n) | Numerischer Vergleich |
.toBeNull() / .toBeDefined() | Nullability Checks |
.toHaveBeenCalledWith(args) | Mock-Aufruf verifizieren |
Snapshot Tests
import { describe, it, expect } from 'vitest' import { render } from '@testing-library/react' import { UserCard } from './UserCard' describe('UserCard Snapshots', () => { it('rendert korrekt', () => { const { container } = render( <UserCard name="Alice" role="Admin" /> ) // Snapshot wird beim ersten Lauf erstellt // Bei Änderungen: npx vitest run --update-snapshots expect(container).toMatchSnapshot() }) it('inline snapshot', () => { const user = { id: 1, name: 'Bob' } expect(user).toMatchInlineSnapshot(` Object { "id": 1, "name": "Bob", } `) }) })
3. Mocking — vi.fn(), vi.spyOn(), vi.mock()
Vitest bündelt alles im vi-Objekt — kein separates jest-Namespace mehr.
Claude Code erkennt automatisch welche Module gemockt werden müssen und generiert die
entsprechenden vi.mock()-Blöcke.
vi.fn() — Mock-Funktionen
import { vi, describe, it, expect } from 'vitest' describe('vi.fn() Basics', () => { it('tracked Aufrufe', () => { const mockFn = vi.fn() mockFn('hello') mockFn('world') expect(mockFn).toHaveBeenCalledTimes(2) expect(mockFn).toHaveBeenCalledWith('hello') expect(mockFn).toHaveBeenLastCalledWith('world') }) it('Rückgabewert definieren', () => { const getPrice = vi.fn() .mockReturnValueOnce(9.99) .mockReturnValueOnce(14.99) .mockReturnValue(0) // default danach expect(getPrice()).toBe(9.99) expect(getPrice()).toBe(14.99) expect(getPrice()).toBe(0) }) })
vi.spyOn() — Methoden überwachen
import { vi, afterEach, describe, it, expect } from 'vitest' import * as api from '../services/api' describe('vi.spyOn()', () => { afterEach(() => { vi.restoreAllMocks() // Originale wiederherstellen }) it('überwacht console.log', () => { const spy = vi.spyOn(console, 'log').mockImplementation(() => {}) myFunction() expect(spy).toHaveBeenCalledWith('expected log message') }) it('überwacht API-Methode', () => { const spy = vi.spyOn(api, 'fetchUser') .mockResolvedValue({ id: 1, name: 'Alice' }) // Test läuft, api.fetchUser gibt Mock-Data zurück expect(spy).toHaveBeenCalledOnce() }) })
vi.mock() — ganze Module mocken
import { vi, describe, it, expect } from 'vitest' // vi.mock() wird gehoisted — läuft VOR den Imports! vi.mock('../services/emailService', () => ({ sendEmail: vi.fn().mockResolvedValue({ success: true }), validateEmail: vi.fn(email => email.includes('@')), })) import { sendEmail } from '../services/emailService' import { registerUser } from '../services/userService' describe('User Registration', () => { it('sendet Willkommens-Email', async () => { await registerUser({ email: 'alice@test.de', name: 'Alice' }) expect(sendEmail).toHaveBeenCalledWith({ to: 'alice@test.de', subject: 'Willkommen!', }) }) })
vi.hoisted() — Variablen vor Mock-Hoisting
const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn(), })) vi.mock('nodemailer', () => ({ default: { createTransport: () => ({ sendMail: mockSend }), }, })) // Jetzt kann mockSend in Tests geprüft werden it('ruft sendMail auf', async () => { await sendWelcomeEmail('user@test.de') expect(mockSend).toHaveBeenCalledOnce() })
4. Async Tests — async/await, Fake Timers
Asynchrone Tests sind in Vitest besonders elegant: async/await,
.resolves/.rejects Modifier und leistungsfähige Fake Timers
für setInterval/setTimeout-Testing.
async/await und resolves/rejects
import { describe, it, expect } from 'vitest' import { fetchUserData, deleteUser } from '../api/users' describe('Async API Tests', () => { it('lädt User-Daten', async () => { const user = await fetchUserData(42) expect(user).toMatchObject({ id: 42, name: expect.any(String) }) }) // resolves/rejects = eleganter für Promise-Chains it('resolves mit User-Objekt', () => { return expect(fetchUserData(1)).resolves.toHaveProperty('email') }) it('rejects bei nicht-existentem User', () => { return expect(fetchUserData(99999)) .rejects .toThrow('User not found') }) it('async/await mit rejects', async () => { await expect(deleteUser(-1)) .rejects .toMatchObject({ code: 'INVALID_ID' }) }) })
Fake Timers — setTimeout/setInterval testen
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { Debouncer } from '../utils/debounce' describe('Fake Timers', () => { beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('debounce feuert nach 500ms', () => { const callback = vi.fn() const debounced = new Debouncer(callback, 500) debounced.call('arg1') expect(callback).not.toHaveBeenCalled() vi.advanceTimersByTime(499) expect(callback).not.toHaveBeenCalled() vi.advanceTimersByTime(1) // jetzt 500ms voll expect(callback).toHaveBeenCalledWith('arg1') }) it('alle Timer auf einmal ausführen', () => { const fn1 = vi.fn(), fn2 = vi.fn() setTimeout(fn1, 1000) setTimeout(fn2, 5000) vi.runAllTimers() expect(fn1).toHaveBeenCalled() expect(fn2).toHaveBeenCalled() }) it('Datum/Zeit mocken', () => { vi.setSystemTime(new Date('2026-01-01')) expect(new Date().getFullYear()).toBe(2026) }) })
waitFor und polling
import { waitFor } from '@testing-library/react' it('wartet auf asynchrones DOM-Update', async () => { render(<AsyncComponent />) // Wartet bis Bedingung true ist (default timeout: 1000ms) await waitFor(() => { expect(screen.getByText('Daten geladen')).toBeInTheDocument() }, { timeout: 2000, interval: 100 }) })
5. Coverage — @vitest/coverage-v8
Vitest unterstützt zwei Coverage-Provider: v8 (schneller, V8-intern) und
istanbul (genauer bei komplexen Transforms). Für die meisten Projekte
ist v8 die bessere Wahl.
Coverage starten
# Einmalig installieren (noch nicht installiert?) npm install -D @vitest/coverage-v8 # Coverage Report generieren npx vitest run --coverage # Mit istanbul statt v8 npm install -D @vitest/coverage-istanbul npx vitest run --coverage --coverage.provider=istanbul
Coverage-Konfiguration — vollständig
// vitest.config.ts oder vite.config.ts export default defineConfig({ test: { coverage: { // Provider: 'v8' (default, schnell) oder 'istanbul' (genau) provider: 'v8', // Reporter: 'text' (terminal), 'html' (browser), 'lcov' (CI), 'json' reporter: ['text', 'html', 'lcov', 'json-summary'], // Output-Verzeichnis reportsDirectory: './coverage', // Welche Dateien einschließen include: ['src/**/*.{ts,tsx}'], exclude: [ 'src/**/*.d.ts', 'src/test/**', 'src/**/*.stories.*', 'src/main.tsx', 'src/vite-env.d.ts', ], // Thresholds — CI schlägt fehl wenn unterschritten thresholds: { lines: 80, functions: 80, branches: 75, statements: 80, // Per-File threshold perFile: false, }, // Alle Quelldateien anzeigen, auch untestete all: true, // Branch-Coverage in V8 aktivieren experimentalAstAwareRemapping: true, }, }, })
| Merkmal | @vitest/coverage-v8 | @vitest/coverage-istanbul |
|---|---|---|
| Geschwindigkeit | ⚡ Sehr schnell | 🐢 Langsamer |
| Genauigkeit | ✅ Gut | ✅✅ Sehr genau |
| Dependency | Keine (V8 builtin) | istanbul-lib-* |
| Source Maps | V8 native | Instrument-basiert |
| Empfehlung | Vite-Projekte | Legacy / komplexe Transforms |
Coverage in CI (GitHub Actions)
# .github/workflows/test.yml - name: Run Tests with Coverage run: npx vitest run --coverage - name: Upload Coverage Report uses: codecov/codecov-action@v4 with: file: ./coverage/lcov.info fail_ci_if_error: true
thresholds lässt den vitest-Prozess mit Exit-Code 1 enden wenn die Schwelle unterschritten wird — damit schlägt die CI-Pipeline fehl. Das ist gewollt!
6. Vitest UI und Migration von Jest
Vitest UI — Interaktiver Test-Browser
# Starten npx vitest --ui # Oder via package.json script "test:ui": "vitest --ui" # Öffnet automatisch http://localhost:51204/__vitest__/
Das Vitest UI zeigt alle Tests in einer Baumstruktur, ermöglicht einzelne Tests zu starten, Coverage-Berichte direkt im Browser und bietet einen Dependency-Graph. Besonders hilfreich beim Debugging komplexer Test-Suites.
Watch-Modus und Bench
# Watch-Modus (default wenn kein 'run') npx vitest # Nur geänderte Tests laufen lassen npx vitest --changed # Spezifische Datei npx vitest src/utils/pricing.test.ts # Benchmark Tests (bench statt test) npx vitest bench
Benchmark-Tests (neu in Vitest 2.x)
import { bench, describe } from 'vitest' describe('Sort Algorithmen', () => { const arr = Array.from({ length: 1000 }, () => Math.random()) bench('Array.sort (native)', () => { [...arr].sort((a, b) => a - b) }) bench('Quicksort (custom)', () => { quickSort([...arr]) }) })
Migration von Jest → Vitest
# @vitest/jest-migration (experimentell) npx @vitest/jest-migration # Oder manuell: @jest-community/codemod npx @jest-community/codemod jest-to-vitest --extensions ts,tsx src/
Migration-Checkliste
| Jest | Vitest Equivalent | Aktion |
|---|---|---|
jest.fn() | vi.fn() | Suchen & Ersetzen |
jest.spyOn() | vi.spyOn() | Suchen & Ersetzen |
jest.mock() | vi.mock() | Suchen & Ersetzen |
jest.useFakeTimers() | vi.useFakeTimers() | Suchen & Ersetzen |
jest.config.js | vite.config.ts test:{} | Merge + löschen |
@types/jest | vitest/globals | tsconfig anpassen |
babel-jest | Nicht nötig | Dependency entfernen |
ts-jest | Nicht nötig | Dependency entfernen |
tsconfig.json nach Migration
{
"compilerOptions": {
"types": ["vitest/globals"],
// entfernen: "@types/jest"
}
}
Claude Code automatisiert dein Testing
Von Setup bis Coverage-Report — Claude Code schreibt Vitest-Tests im Kontext deiner Codebase, erkennt Edge Cases und generiert Mocks für externe Abhängigkeiten. Kostenlose Trial starten.
Kostenlos testen — Trial startenFazit
Vitest ist 2026 der Standard für TypeScript-Unit-Testing in Vite-Projekten — schneller als Jest, einfacher zu konfigurieren und mit nativer ESM/TypeScript-Unterstützung. Die Kombination aus vi.fn()/vi.mock() für Mocking, coverage-v8 für Reports und dem UI-Mode für interaktives Debugging macht Testing produktiv statt mühsam.
Claude Code beschleunigt den gesamten TDD-Zyklus: Tests generieren, Mocks erstellen, Coverage-Lücken schließen — alles aus dem Editor-Kontext heraus, ohne Boilerplate von Hand tippen zu müssen.