Testing & Qualität

Vitest mit Claude Code:
Unit Testing 2026

Vitest als schnellere Jest-Alternative — Claude Code schreibt Unit Tests mit Mocking, Coverage und TypeScript-Support direkt aus der Vite-Config.

Vitest Mocking Coverage Async TypeScript Migration
📅 6. Mai 2026 ⏱ 10 min Lesezeit 🧪 Claude Code Guide

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.

Warum Vitest statt Jest?
FeatureVitestJest
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()
})
Claude Code Prompt: "Erstelle eine vollständige Vitest-Config für mein Vite+React+TypeScript-Projekt mit jsdom, globals:true, @testing-library/jest-dom setup und 80% Coverage-Threshold."

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 Übersicht
MatcherVerwendung
.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()
})
Claude Code Prompt: "Mocke das Modul '../api/stripe' komplett — vi.mock() mit mockResolvedValue für createPaymentIntent und confirmPayment. Dann schreibe Tests für checkout.ts die prüfen, ob beide Methoden mit den richtigen Parametern aufgerufen werden."

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,
    },
  },
})
v8 vs. istanbul — Vergleich
Merkmal@vitest/coverage-v8@vitest/coverage-istanbul
Geschwindigkeit⚡ Sehr schnell🐢 Langsamer
Genauigkeit✅ Gut✅✅ Sehr genau
DependencyKeine (V8 builtin)istanbul-lib-*
Source MapsV8 nativeInstrument-basiert
EmpfehlungVite-ProjekteLegacy / 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
Achtung: 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

Automatische Migration
# @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

JestVitest EquivalentAktion
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.jsvite.config.ts test:{}Merge + löschen
@types/jestvitest/globalstsconfig anpassen
babel-jestNicht nötigDependency entfernen
ts-jestNicht nötigDependency entfernen

tsconfig.json nach Migration

{
  "compilerOptions": {
    "types": ["vitest/globals"],
    // entfernen: "@types/jest"
  }
}
Claude Code Prompt für Migration: "Migriere alle *.test.ts Dateien in src/ von Jest zu Vitest. Ersetze jest.fn mit vi.fn, jest.mock mit vi.mock, @jest/globals Imports mit vitest. Erstelle eine neue vite.config.ts mit dem test-Block. Lösche jest.config.js."

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 starten

Fazit

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.