Testing & QA · 5. Mai 2026

Vitest mit Claude Code:
Blitzschnelle Unit-Tests 2026

📅 5. Mai 2026 ⏱ 10 min Lesezeit 🧪 Testing & QA

Vitest hat sich 2026 als der De-facto-Standard für Unit-Tests in Vite-Projekten etabliert. Blitzschnelle Ausführung, native ESM-Unterstützung, eine Jest-kompatible API und ein hervorragendes Developer-Experience machen ihn zur ersten Wahl für moderne TypeScript-Projekte — und mit Claude Code als KI-Copiloten wird Test-driven Development so flüssig wie nie zuvor.

In diesem Guide zeige ich dir, wie du Vitest von Null zum produktionsreifen Test-Setup bringst: Setup in vite.config.ts, Mocking mit vi, React Testing Library Integration, Coverage-Konfiguration und das interaktive Vitest UI. Claude Code übernimmt dabei die Boilerplate, schlägt Edge Cases vor und debuggt rote Tests eigenständig.

Vitest vs. Jest 2026 — schneller Vergleich

Feature Vitest Jest
Native ESM ✓ nativ ✗ Transform nötig
Start-Geschwindigkeit ✓ <100 ms ∅ 1–3 s
Vite-Config geteilt ✓ ja ✗ separate Config
In-Source Testing ✓ ja ✗ nein
Browser Mode ✓ experimentell ∅ via Jest-env-jsdom
TypeScript ✓ ohne Config ∅ ts-jest nötig
Watch Mode ✓ HMR-basiert ∅ stabiler
Claude Code Workflow-Tipp Öffne Claude Code im Projektverzeichnis und schreibe: "Erstelle mir ein vollständiges Vitest-Setup mit Coverage und React Testing Library." Claude analysiert deine package.json, passt die vite.config.ts an und generiert Beispiel-Tests — komplett ohne Copy-Paste.
01 · Setup

Vitest Setup: vite.config.ts

Vitest ist nahtlos in das Vite-Ökosystem integriert. Die Test-Konfiguration landet direkt in der vite.config.ts unter dem test-Schlüssel — keine zweite Konfigurationsdatei, kein doppeltes TS-Config-Management.

Installation

bash # Mit npm npm install -D vitest @vitest/coverage-v8 @vitest/ui jsdom # Mit pnpm (empfohlen in Monorepos) pnpm add -D vitest @vitest/coverage-v8 @vitest/ui jsdom # Claude Code Prompt dafür: # "Installiere Vitest mit Coverage und UI-Plugin für dieses Projekt"

vite.config.ts — vollständige Test-Konfiguration

typescript /// <reference types="vitest" /> import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { // Globale Test-APIs ohne Import (describe, it, expect, vi) globals: true, // DOM-Simulation für React-Tests environment: 'jsdom', // Setup-Datei für @testing-library/jest-dom Matcher setupFiles: ['./src/test/setup.ts'], // Welche Dateien als Tests erkannt werden include: [ 'src/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{js,jsx}' ], // Dateien von der Test-Ausführung ausschließen exclude: [ 'node_modules', 'dist', '**/*.e2e.{ts,tsx}' ], // Coverage-Konfiguration coverage: { provider: 'v8', reporter: ['text', 'json', 'lcov', 'html'], reportsDirectory: './coverage', include: ['src/**'], exclude: [ 'src/test/**', 'src/**/*.d.ts', 'src/main.tsx' ], // Mindest-Schwellwerte — CI schlägt fehl wenn unterschritten thresholds: { lines: 80, functions: 80, branches: 70, statements: 80 } }, // Reporter für bessere CI-Ausgabe reporter: 'verbose', // Parallelisierung (false = stabiler auf schwachen CI-Rechnern) pool: 'forks', poolOptions: { forks: { singleFork: false } } } })

Setup-Datei für jest-dom

typescript // src/test/setup.ts import '@testing-library/jest-dom' // Globale Mocks die in jedem Test verfügbar sein sollen import { vi } from 'vitest' // fetch global mocken (jsdom hat keinen nativen fetch) import { fetch, Headers, Request, Response } from 'node-fetch' vi.stubGlobal('fetch', fetch) vi.stubGlobal('Headers', Headers)

package.json Scripts

json { "scripts": { "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "test:ci": "vitest run --reporter=junit --outputFile=test-results.xml" } }
01
Installieren
vitest + @vitest/coverage-v8 + jsdom
02
Konfigurieren
test-Block in vite.config.ts
03
Setup-Datei
jest-dom + globale Mocks
04
npm test
Erster Testlauf in <100 ms
02 · Grundlagen

Grundlagen: describe, it, expect

Die Vitest-API ist vollständig Jest-kompatibel. Wer Jest kennt, ist sofort produktiv. Mit aktivierten globals: true in der Konfiguration entfallen alle Imports — Claude Code generiert Tests ohne Import-Boilerplate.

Grundstruktur eines Tests

typescript // src/utils/math.test.ts // Mit globals:true: kein Import von describe/it/expect nötig! describe('Math Utilities', () => { it('addiert zwei positive Zahlen korrekt', () => { expect(add(2, 3)).toBe(5) }) it('wirft einen Fehler bei Division durch Null', () => { expect(() => divide(10, 0)).toThrow('Division durch Null nicht erlaubt') }) it('verarbeitet negative Zahlen', () => { expect(add(-5, 3)).toBe(-2) expect(add(-5, -3)).toBe(-8) }) })

beforeEach / afterEach / beforeAll / afterAll

typescript // src/services/userService.test.ts import { UserService } from './userService' import { db } from '../db' describe('UserService', () => { let service: UserService beforeAll(async () => { // Einmalige Initialisierung: DB-Verbindung aufbauen await db.connect() }) afterAll(async () => { await db.disconnect() }) beforeEach(() => { // Vor jedem Test: frische Instanz, leere State service = new UserService() }) afterEach(async () => { // Nach jedem Test: Test-Daten bereinigen await db.query('DELETE FROM users WHERE email LIKE \'%@test.com\'') }) it('erstellt einen neuen User', async () => { const user = await service.create({ name: 'Test User', email: 'user@test.com' }) expect(user).toMatchObject({ name: 'Test User', id: expect.any(String) }) }) })

test.each — Parameterized Tests

Parameterized Tests sind einer der produktivsten Aspekte von Vitest. Claude Code kann auf Basis einer Funktionssignatur automatisch vollständige Testmatrizen generieren — mit Edge Cases, die Menschen gerne vergessen.

typescript // Tabellen-Syntax (empfohlen für Lesbarkeit) test.each([ ['hello world', 'Hello World'], ['foo bar baz', 'Foo Bar Baz'], ['', '' ], ['ALREADY CAPS', 'Already Caps'], ])('capitalize(%s) === %s', (input, expected) => { expect(capitalize(input)).toBe(expected) }) // Objekt-Syntax mit Template-Literal-Beschreibung test.each([ { a: 1, b: 2, expected: 3 }, { a: -1, b: 1, expected: 0 }, { a: 0, b: 0, expected: 0 }, ])('add($a, $b) = $expected', ({ a, b, expected }) => { expect(add(a, b)).toBe(expected) }) // describe.each für ganze Test-Gruppen describe.each([ ['production', { debug: false, cache: true }], ['development', { debug: true, cache: false }], ])('Config in %s mode', (env, config) => { it('hat korrekten debug-Wert', () => { expect(getConfig(env).debug).toBe(config.debug) }) })
Claude Code Prompt für test.each "Generiere einen parametrisierten Vitest-Test für die Funktion `validateEmail(email: string): boolean` mit mindestens 10 Testfällen — gültige Emails, ungültige, Edge Cases wie leere Strings und internationale Domains."
03 · Mocking

Mocking: vi.mock, vi.fn, vi.spyOn

Vitest's vi-Objekt ist das Äquivalent zu Jest's jest-Objekt und bietet identische Mocking-Fähigkeiten — aber deutlich schneller und mit besserer TypeScript-Integration. Claude Code kann komplexe Mock-Setups aus einfachen Beschreibungen generieren.

vi.mock — Modul-Mocking

typescript // WICHTIG: vi.mock-Aufrufe werden automatisch gehoisted (ans Datei-Anfang)! // Das ist Vitest's automatisches Hoisting — genau wie bei Jest vi.mock('../api/userApi', () => ({ fetchUser: vi.fn().mockResolvedValue({ id: '123', name: 'Max Mustermann', email: 'max@example.com' }), updateUser: vi.fn().mockResolvedValue({ success: true }) })) // Teilweises Mocking — nur einzelne Exports ersetzen vi.mock('../utils/dateUtils', async (importOriginal) => { const original = await importOriginal<typeof import('../utils/dateUtils')>() return { ...original, // alle echten Exporte behalten getNow: vi.fn().mockReturnValue(new Date('2026-05-05T12:00:00Z')) } })

vi.fn — Mock-Funktionen

typescript describe('vi.fn Beispiele', () => { it('trackt Aufrufe und Argumente', () => { const mockFn = vi.fn() mockFn('hello', 42) mockFn('world') expect(mockFn).toHaveBeenCalledTimes(2) expect(mockFn).toHaveBeenCalledWith('hello', 42) expect(mockFn).toHaveBeenLastCalledWith('world') }) it('gibt definierte Werte zurück', () => { const mockFn = vi.fn() .mockReturnValueOnce('first') // Erster Aufruf .mockReturnValueOnce('second') // Zweiter Aufruf .mockReturnValue('default') // Alle weiteren expect(mockFn()).toBe('first') expect(mockFn()).toBe('second') expect(mockFn()).toBe('default') expect(mockFn()).toBe('default') }) it('simuliert asynchrone Operationen', async () => { const fetchData = vi.fn() .mockResolvedValueOnce({ data: [1, 2, 3] }) .mockRejectedValueOnce(new Error('Netzwerkfehler')) const result = await fetchData() expect(result.data).toHaveLength(3) await expect(fetchData()).rejects.toThrow('Netzwerkfehler') }) })

vi.spyOn — Spies auf echte Objekte

typescript describe('vi.spyOn', () => { afterEach(() => { vi.restoreAllMocks() // Immer nach dem Test aufräumen! }) it('überwacht console.error ohne es zu unterdrücken', () => { const consoleSpy = vi.spyOn(console, 'error') myFunctionThatMightLogError() expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('Validation failed') ) }) it('spioniert auf localStorage', () => { const setItemSpy = vi.spyOn(Storage.prototype, 'setItem') saveUserPreference('theme', 'dark') expect(setItemSpy).toHaveBeenCalledWith('theme', 'dark') }) it('mockt Date.now() für deterministischen Zeitstempel', () => { vi.spyOn(Date, 'now').mockReturnValue(1746446400000) // 2026-05-05 const timestamp = createTimestamp() expect(timestamp).toBe('2026-05-05T12:00:00.000Z') }) }) // vi.mocked — TypeScript-sichere Mock-Casts import { fetchUser } from '../api/userApi' vi.mock('../api/userApi') it('setzt Rückgabewert type-sicher', () => { vi.mocked(fetchUser).mockResolvedValue({ id: '42', name: 'Ada Lovelace', email: 'ada@code.io' }) // TypeScript weiß: das ist ein Mock — kein any-Cast nötig })

vi.stubGlobal — Browser-APIs mocken

typescript // ResizeObserver, IntersectionObserver, matchMedia — alle nicht in jsdom beforeEach(() => { vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() }))) vi.stubGlobal('matchMedia', vi.fn(query => ({ matches: query === '(prefers-color-scheme: dark)', addEventListener: vi.fn(), removeEventListener: vi.fn() }))) }) afterEach(() => { vi.unstubAllGlobals() })
Wichtig: vi.mock Hoisting Vitest hoisted vi.mock()-Aufrufe automatisch an den Anfang der Datei — genau wie Jest. Das bedeutet: Variablen die im Modul-Scope definiert wurden, sind bei vi.mock noch nicht verfügbar. Nutze vi.hoisted() wenn du Mock-Factories mit Variablen brauchst.
04 · React Testing Library

React Testing Library Integration

React Testing Library und Vitest sind das Dream-Team für Component-Tests 2026. Mit @testing-library/user-event simulierst du echte Nutzerinteraktionen, waitFor handhabt async State-Updates, und screen macht Tests lesbar wie Prosa.

Installation

bash npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom

Einfacher Component-Test

tsx // src/components/Button.test.tsx import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Button } from './Button' describe('Button Komponente', () => { it('rendert mit korrektem Label', () => { render(<Button label="Klick mich" />) expect(screen.getByRole('button', { name: 'Klick mich' })).toBeInTheDocument() }) it('ruft onClick Handler auf', async () => { const handleClick = vi.fn() const user = userEvent.setup() render(<Button label="Test" onClick={handleClick} />) await user.click(screen.getByRole('button')) expect(handleClick).toHaveBeenCalledOnce() }) it('ist deaktiviert wenn disabled prop gesetzt', () => { render(<Button label="Gesperrt" disabled />) expect(screen.getByRole('button')).toBeDisabled() }) })

Formular-Test mit userEvent

tsx // src/components/LoginForm.test.tsx import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { LoginForm } from './LoginForm' describe('LoginForm', () => { const setup = () => { const user = userEvent.setup() const onSubmit = vi.fn() render(<LoginForm onSubmit={onSubmit} />) return { user, onSubmit } } it('submits with valid credentials', async () => { const { user, onSubmit } = setup() await user.type( screen.getByLabelText(/email/i), 'max@example.com' ) await user.type( screen.getByLabelText(/passwort/i), 'sicheresPasswort123!' ) await user.click(screen.getByRole('button', { name: /anmelden/i })) await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: 'max@example.com', password: 'sicheresPasswort123!' }) }) }) it('zeigt Validierungsfehler bei leerem Formular', async () => { const { user } = setup() await user.click(screen.getByRole('button', { name: /anmelden/i })) expect(await screen.findByText(/email ist erforderlich/i)).toBeVisible() }) it('zeigt Lade-Spinner während Login', async () => { const { user, onSubmit } = setup() onSubmit.mockImplementation( () => new Promise(r => setTimeout(r, 100)) ) await user.type(screen.getByLabelText(/email/i), 'a@b.com') await user.click(screen.getByRole('button', { name: /anmelden/i })) expect(screen.getByTestId('loading-spinner')).toBeVisible() await waitFor(() => expect(screen.queryByTestId('loading-spinner')).toBeNull()) }) })

Custom Render-Wrapper mit Context/Provider

tsx // src/test/utils.tsx — wiederverwendbarer Render-Wrapper import { render, RenderOptions } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { MemoryRouter } from 'react-router-dom' const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }) export function renderWithProviders( ui: React.ReactElement, { route = '/', ...options }: { route?: string } & RenderOptions = {} ) { const queryClient = createTestQueryClient() function Wrapper({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> <MemoryRouter initialEntries={[route]}> {children} </MemoryRouter> </QueryClientProvider> ) } return { ...render(ui, { wrapper: Wrapper, ...options }), queryClient } }
getBy vs queryBy vs findBy — die Unterschiede getBy: wirft wenn nicht gefunden (synchron) — für Elemente die immer da sein müssen. queryBy: gibt null zurück wenn nicht gefunden — für "ist NICHT da"-Checks. findBy: gibt Promise zurück — für Elemente die erst nach async-Operationen erscheinen (intern waitFor).
05 · Coverage

Coverage: v8, istanbul, lcov für CI

Vitest unterstützt zwei Coverage-Provider: v8 (schneller, nutzt V8's nativen Coverage-Mechanismus) und istanbul (mehr Konfigurationsoptionen, breiter unterstützt). Für die meisten Projekte ist v8 die bessere Wahl.

Provider-Vergleich

Kriterium v8 istanbul
Performance ✓ schneller ∅ etwas langsamer
Genauigkeit ∅ gut ✓ präziser
Source Maps ✓ nativ ✓ ja
Extra-Package @vitest/coverage-v8 @vitest/coverage-istanbul
Empfehlung ✓ Standard 2026 ∅ Legacy/spezielle Needs

Vollständige Coverage-Konfiguration

typescript // vite.config.ts — coverage-Block coverage: { provider: 'v8', // Reporter: text (Terminal), json (CI), lcov (Codecov/SonarQube), html (Browser) reporter: ['text', 'text-summary', 'json', 'lcov', 'html'], // Output-Verzeichnis reportsDirectory: './coverage', // Was gemessen wird include: ['src/**/*.{ts,tsx}'], exclude: [ 'src/**/*.d.ts', 'src/test/**', 'src/main.tsx', 'src/**/*.stories.{ts,tsx}', 'src/**/__mocks__/**' ], // Schwellwerte: CI schlägt bei Unterschreitung fehl thresholds: { // Global-Schwellwerte lines: 80, functions: 80, branches: 70, statements: 80, // Per-File-Schwellwerte für kritische Dateien perFile: true, // Auto-Update: Schlägt fehl wenn Coverage SINKT (ratchet) autoUpdate: false // true = schreibt neue Schwellwerte nach Coverage-Erhöhung }, // Alle Quelldateien einschließen, auch ohne Tests all: true, // Branch-Coverage aktivieren (v8-spezifisch) clean: true // Coverage-Verzeichnis vor jedem Run löschen }

GitHub Actions CI-Integration

yaml # .github/workflows/test.yml name: Tests & Coverage on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - name: Install Dependencies run: npm ci - name: Run Tests with Coverage run: npm run test:coverage - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 with: files: ./coverage/lcov.info fail_ci_if_error: true - name: Upload Coverage HTML Report uses: actions/upload-artifact@v4 if: always() with: name: coverage-report path: coverage/

Coverage-Kommentare in Code ausschließen

typescript // Einzelne Zeile ausschließen: const impossibleBranch = true /* c8 ignore next */ // Block ausschließen: /* c8 ignore start */ if (process.env.NODE_ENV === 'development') { console.debug('Debug-only Code') } /* c8 ignore stop */ // Funktion komplett ausschließen: function onlyForManualTesting() { /* c8 ignore next */ // ... }
Claude Code Coverage-Analyse "Analysiere meine Coverage-Lücken in src/services/ und schreibe Tests für die Funktionen mit unter 70% Branch Coverage." Claude liest den HTML-Report, identifiziert die Lücken und generiert gezielt Tests für ungetestete Branches — inklusive Error-Pfade.
06 · Vitest UI & Watch Mode

Vitest UI und Watch Mode

Vitest bietet drei Entwicklungsmodi: den klassischen Watch Mode der Tests bei Dateiänderungen neu ausführt, das grafische Vitest UI als Browser-basierte Test-IDE, und den experimentellen Browser Mode der Tests im echten Browser laufen lässt.

Watch Mode — HMR-basiertes Neuausführen

bash # Startet Watch Mode (Standard wenn keine --run Flag) npx vitest # Nur bestimmte Dateien watchen npx vitest src/components/ # Mit Pattern-Filter npx vitest --reporter=verbose --testNamePattern="Button" # Nur geänderte Dateien (wie git diff) npx vitest --changed # Watch Mode Keyboard-Shortcuts (interaktiv): # f → Fehlgeschlagene Tests nochmal ausführen # a → Alle Tests ausführen # p → Datei-Pattern filtern # t → Test-Name-Pattern filtern # q → Beenden

Vitest UI — Browser-basierte Test-IDE

bash # @vitest/ui installieren (einmalig) npm install -D @vitest/ui # UI starten (öffnet automatisch http://localhost:51204/__vitest__/) npx vitest --ui # Mit Coverage im UI npx vitest --ui --coverage

Das Vitest UI bietet eine vollständige Test-Übersicht im Browser: Datei-Baum, Test-Ergebnisse, Fehler-Stack-Traces, Coverage-Visualisierung und einen interaktiven Module-Graph. Besonders hilfreich beim Debuggen komplexer Mock-Setups.

vite.config.ts UI-Konfiguration

typescript test: { // UI-spezifische Einstellungen ui: true, open: true, // Browser automatisch öffnen // API-Port für UI anpassen uiBase: '/__vitest__/', // HTML-Reporter für statische Reports reporters: ['default', 'html'], outputFile: { html: './test-results/index.html' } }

Browser Mode (experimentell)

bash npm install -D @vitest/browser playwright
typescript // vite.config.ts — Browser Mode test: { browser: { enabled: true, name: 'chromium', provider: 'playwright', headless: true, // Nur bestimmte Test-Dateien im Browser ausführen include: ['src/**/*.browser.test.{ts,tsx}'] } } // Browser-Test-Datei mit page-Utilities import { page } from '@vitest/browser/context' test('zeigt Inhalt nach Click', async () => { render(<MyComponent />) await page.getByRole('button').click() await expect.element(page.getByText('Inhalt geladen')).toBeVisible() })

Snapshot-Tests mit Vitest

tsx // Inline Snapshots — direkt im Test-File gespeichert it('rendert Card korrekt', () => { const { container } = render(<Card title="Test" content="Inhalt" />) expect(container.firstChild).toMatchSnapshot() }) // Inline Snapshot (wird automatisch befüllt beim ersten Run) it('serialisiert User-Objekt', () => { const user = { id: '1', name: 'Max', role: 'admin' } expect(user).toMatchInlineSnapshot(` { "id": "1", "name": "Max", "role": "admin", } `) }) // Snapshots aktualisieren: // npx vitest run --update-snapshots (oder: -u)
--ui
Vitest UI
Browser-IDE mit Module Graph und Coverage
--watch
Watch Mode
HMR-basiert, nur geänderte Tests
--browser
Browser Mode
Tests im echten Chromium/Firefox
-u
Update Snapshots
Alle Snapshots neu generieren
Browser Mode in CI Der Browser Mode benötigt Playwright als Dependency und einen installierten Browser. In GitHub Actions: npx playwright install --with-deps chromium VOR dem Test-Run ausführen. Browser Mode ist für Tests die echte DOM-APIs wie WebGL, Web Audio oder CSS Animations brauchen — für normale React-Tests reicht jsdom.

Vitest-Tests mit KI schreiben

Claude Code analysiert dein Projekt, erkennt Testing-Patterns und generiert vollständige Test-Suites — inklusive Mocks, Edge Cases und Coverage für kritische Pfade. Teste es selbst kostenlos.

Kostenlos mit Claude Code starten →
Zusammenfassung

Das Vitest-Setup 2026 auf einen Blick

Der Claude Code Vorteil Claude Code kennt die gesamte Vitest-API und kann aus einer Funktionssignatur vollständige Test-Suites generieren, fehlgeschlagene Tests debuggen, Mock-Strategies vorschlagen und Coverage-Lücken automatisch schließen — ohne dass du dazu die Dokumentation lesen musst.