Design wechseln

Vitest Unit-Tests in der Praxis: Von der Konfiguration zum TDD-Workflow

Easton editorial illustration: test lane checkpoint

Wie lange brauchen Sie, um Jest in einem ESM-Projekt zum Laufen zu bringen? ts-jest, babel-jest, jest.config.js … und dann noch all die Modulauflösungs-Probleme. Ich habe schon einen ganzen Nachmittag damit verbracht, damit Jest .vue-Importe korrekt erkennt.

Bei Vitest reicht oft eine Zeile Konfiguration.

Das ist keine Übertreibung. Als ich in einem Vite-Projekt zum ersten Mal vitest startete und die Tests in wenigen Sekunden durchliefen, fühlte es sich an wie der Wechsel von einem drei Jahre alten, trägen Rechner – erfrischend.

Wie schnell ist Vitest? Offizielle Daten und Community-Messungen zeigen: Cold Start etwa 200 ms (Jest: 2–4 Sekunden), 500 Testfälle in rund 8 Sekunden (Jest: etwa 45 Sekunden). Dazu teilt es die Vite-Konfiguration, unterstützt TypeScript nativ und die API gleicht Jest fast vollständig – die Migration dauert oft nur eine halbe Stunde.

Dieser Artikel führt Sie von der Grundkonfiguration bis zum vollständigen TDD-Workflow – inklusive Mocking und Coverage. Ob neues Vitest-Projekt oder Migration von Jest: Hier finden Sie, was Sie brauchen.

Was ist Vitest – und warum ist es so schnell?

Kurz gesagt: Vitest ist das native Testframework von Vite.

Wenn Sie bereits mit Vite bauen, ist Vitest praktisch sofort einsatzbereit. Es übernimmt die Vite-Konfiguration – Aliase, Umgebungsvariablen, CSS-Verarbeitung. Kein extra Jest-Setup mit transform, moduleFileExtensions, moduleNameMapper.

Die drei Kernvorteile:

Geschwindigkeit. Cold Start etwa 200 Millisekunden, Jest braucht typischerweise 2–4 Sekunden. In großen Projekten wird der Unterschied deutlich: 500 Tests in ~8 Sekunden bei Vitest, ~45 Sekunden bei Jest (Messungen der DEV Community, 2026). Kein marginaler Vorsprung.

Jest-Kompatibilität. Die API ist nahezu identisch – describe, it, expect, vi.fn(), Syntax wie bei Jest. Migration heißt meist: Import-Pfade anpassen.

Intelligentes Watching. „HMR for tests“: Nach Codeänderungen laufen nur betroffene Tests neu, nicht die gesamte Suite. Sofortiges Feedback beim Entwickeln.

Genug zur Theorie – als Nächstes die Installation.

Installation und Konfiguration

Systemvoraussetzungen: Vite ≥ 6.0.0, Node ≥ 20.0.0. In aktuellen Projekten sind diese Versionen meist schon erfüllt.

Installation

Ein Befehl:

npm install -D vitest

Das war’s. Kein @types/jest, ts-jest, jest-environment-jsdom … Vitest unterstützt TypeScript nativ.

Konfigurationsdatei

Zwei Wege: test-Feld in vite.config.ts oder separate vitest.config.ts.

Für einfache Projekte reicht vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,  // Globale Variablen, kein Import von describe, it, expect nötig
    environment: 'node', // oder 'jsdom' (Browser-Umgebung)
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
    },
  },
})

globals: true ist praktisch – danach sind describe, it, expect global verfügbar, wie bei Jest. Kein Import pro Testdatei.

Für DOM-APIs (z. B. Komponenten-Rendering) environment auf 'jsdom' setzen und jsdom installieren:

npm install -D jsdom

Run-Skripte

In package.json zwei Zeilen ergänzen:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

npm test startet den Watch-Modus (Tests laufen bei Änderungen neu), npm run test:run führt einmal aus und beendet (für CI).

So schlank im Vergleich zu Jest mit preset, transform, moduleFileExtensions.

Unit-Tests schreiben

Testdateien heißen üblicherweise .test.ts oder .spec.ts, im Ordner tests/ oder neben der Quelldatei – je nach Teamkonvention.

Grundstruktur

Ein minimaler Test:

import { describe, it, expect } from 'vitest'
import { add, divide } from './math'

describe('Math utilities', () => {
  it('should add two numbers', () => {
    expect(add(2, 3)).toBe(5)
  })

  it('should throw on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero')
  })
})

describe gruppiert Tests, it definiert einen Fall, expect prüft das Ergebnis.

Mit globals: true entfällt die Import-Zeile.

Häufige Assertions

Die wichtigsten:

// Gleichheit
expect(value).toBe(5)            // Strikte Gleichheit (===)
expect(obj).toEqual({ a: 1 })    // Tiefe Gleichheit

// Wahrheitswerte
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()

// Exceptions
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')

// Zahlenvergleich
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)

// Array/String
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)

Tests filtern

Nur einen Test ausführen? .only:

it.only('nur dieser Test', () => { ... })

Einen Test überspringen? .skip:

it.skip('vorerst auslassen', () => { ... })

Gilt auch für describe: describe.only(...), describe.skip(...).

Im Watch-Modus sehen Sie nach jeder Änderung sofort grün oder rot – deutlich angenehmer als bei Jest, wo der Start jedes Mal mehrere Sekunden kostet.

TDD-Workflow in der Praxis

TDD (Test-Driven Development) bedeutet: zuerst Tests, dann Code.

Das klingt kontraintuitiv, zwingt Sie aber dazu, vor dem Codieren zu klären, was eine Funktion leisten soll – statt nachträglich Tests „für Coverage“ anzuhängen.

Im Folgenden ein vollständiger Durchlauf: eine E-Mail-Validierungsfunktion validateEmail.

Schritt 1: Test schreiben, noch ohne Implementierung

Zuerst die Testdatei:

// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'

describe('validateEmail', () => {
  it('should return true for valid email', () => {
    expect(validateEmail('[email protected]')).toBe(true)
  })

  it('should return false for invalid email', () => {
    expect(validateEmail('invalid')).toBe(false)
  })
})

validateEmail existiert noch nicht – der Test schlägt fehl. Genau das ist TDD-Schritt eins.

Schritt 2: Minimalen Code implementieren

Funktion anlegen, minimal implementieren:

// src/validateEmail.ts
export function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

Tests starten: npm test.

Beide Tests grün? Schritt 2 erledigt.

Schritt 3: Grenzfälle ergänzen

Grundfälle bestehen – E-Mail-Validierung hat mehr Randfälle:

// tests/validateEmail.test.ts (ergänzen)
it('should return false for empty string', () => {
  expect(validateEmail('')).toBe(false)
})

it('should return false for email without domain', () => {
  expect(validateEmail('user@')).toBe(false)
})

it('should return false for email with spaces', () => {
  expect(validateEmail('test @example.com')).toBe(false)
})

Tests laufen – schlagen welche fehl, Regex anpassen.

Schritt 4: Refactoring

Alle Tests grün – jetzt sicher refaktorisieren, z. B. strengeres Regex oder Kommentare:

// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

export function validateEmail(email: string): boolean {
  if (!email || email.trim() === '') {
    return false
  }
  return EMAIL_REGEX.test(email)
}

Erneut testen – weiterhin grün. Das ist der TDD-Vorteil: Refactoring mit Testnetz.

Warum TDD sich lohnt

Am Anfang wirkte „Tests zuerst“ ungewohnt. Nach ein paar Durchläufen wurde klar:

  1. Erst denken, dann coden: Beim Testschreiben definieren Sie Verhalten und Ein-/Ausgaben.
  2. Schnelle Iteration: Vitest Watch-Modus liefert Feedback in Sekunden.
  3. Sicheres Refactoring: Tests fangen Regressionen sofort ab.

Starten Sie mit einfachen Hilfsfunktionen – Formatierung, Validierung. Danach komplexere Logik.

Mocking – fortgeschrittene Techniken

Unit-Tests isolieren oft externe Abhängigkeiten – API-Aufrufe, Datenbank, Drittanbieter. Dafür brauchen Sie Mocks.

Vitest bietet eine Jest-ähnliche Mock-API um das vi-Objekt.

vi.fn(): Einzelne Funktion mocken

Einfachste Variante – Fake-Funktion:

import { vi, describe, it, expect } from 'vitest'

describe('vi.fn() demo', () => {
  it('tracks calls', () => {
    const mockFn = vi.fn()

    mockFn('hello')
    mockFn('world')

    expect(mockFn).toHaveBeenCalledTimes(2)
    expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
  })
})

Rückgabewerte vorgeben:

const mockFn = vi.fn().mockReturnValue('mocked result')
// oder asynchron
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })

vi.mock(): Ganzes Modul mocken

Funktion hängt von externer API ab? Modul mocken:

import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'

// api-Modul mocken
vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))

describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks() // Aufrufprotokoll vor jedem Test leeren
  })

  it('should fetch user', async () => {
    const service = new UserService()
    const user = await service.getUser(1)

    expect(user.name).toBe('Alice')
    expect(fetchUser).toHaveBeenCalledWith(1)
  })
})

vi.mock() läuft vor dem Modulimport – deshalb oben in der Datei platzieren.

vi.spyOn(): Echte Funktion überwachen

Manchmal soll die Funktion weiterlaufen, nur Aufrufe mitprotokolliert werden:

import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'

describe('spyOn demo', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('tracks add calls', () => {
    const addSpy = vi.spyOn(calculator, 'add')

    const result = calculator.add(2, 3)

    expect(result).toBe(5) // Originalfunktion läuft
    expect(addSpy).toHaveBeenCalledWith(2, 3) // Aufruf protokolliert
  })
})

spyOn ist sanfter als mock – Verhalten bleibt, plus Überwachung.

Globale Objekte mocken

Keine echten HTTP-Requests in Tests? Globales fetch mocken:

vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
  ok: true,
  json: () => Promise.resolve({ data: 'mocked' })
}))

Mit vi.stubGlobal auch window, localStorage und andere Browser-Globals.

Mocking ist oft der kniffligste Teil. Starten Sie mit vi.fn(), dann vi.mock(). Nach jedem Test Mocks bereinigen (clearAllMocks oder restoreAllMocks) – sonst vermischen sich Testzustände.

Coverage und Best Practices

Testabdeckung ist ein Qualitätsindikator. Vitest unterstützt zwei Coverage-Provider: v8 (schneller, nativ) und istanbul (breitere Kompatibilität). Meist reicht v8.

Coverage konfigurieren

In vite.config.ts:

test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'html', 'lcov'], // Ausgabeformate
    thresholds: {
      lines: 80,      // Zeilenabdeckung
      functions: 80,  // Funktionsabdeckung
      branches: 70,   // Verzweigungsabdeckung
    },
    exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
  },
}

Ausführen:

vitest run --coverage

Terminal zeigt den Report, im Ordner coverage/ liegt ein HTML-Report für ungetestete Stellen.

Schwellenwerte sinnvoll setzen

thresholds sind aktiv: Unterschreitung führt zum Fehlerabbruch – nützlich in CI, um Qualität zu erzwingen.

Nicht zu hoch ansetzen. 80 % ist ein guter Start; 100 % kostet oft mehr, als es bringt.

CI/CD-Integration

In GitHub Actions oder anderer CI einen Schritt ergänzen:

# .github/workflows/test.yml
- name: Run tests with coverage
  run: npm run test:run -- --coverage

Optional lcov an Codecov oder Coveralls senden, um Trends zu verfolgen.

Praktische Tipps

  1. Nicht 100 % anstreben: Hohe Coverage ≠ hohe Qualität. Kernlogik testen, Randfälle selektiv.
  2. Happy Path zuerst: Hauptfluss hat Priorität, Ausnahmen danach.
  3. Veraltete Tests aufräumen: Tests brauchen Wartung – Redundantes entfernen.
  4. Watch-Modus nutzen: vitest beim Entwickeln laufen lassen, Probleme sofort sehen.

Fazit

Vitest in drei Worten: schnell, unkompliziert, praxisnah.

Schnell – Cold Start 200 ms, 500 Tests in ~8 Sekunden, fast eine Größenordnung schneller als Jest. Unkompliziert – Vite-Konfiguration teilen, installieren und loslegen, ohne transform/moduleNameMapper-Chaos. Praxisnah – Jest-ähnliche API, geringe Migrationskosten.

Mit Vite? Vitest wählen – Jest-ESM-Probleme sparen Sie sich.

Noch auf Jest? Eine halbe Stunde Migration lohnt sich: Vitest installieren, Imports anpassen – oft läuft alles sofort.

TDD nicht überkomplizieren. Mit einfachen Hilfsfunktionen starten, Tests zuerst. Gewöhnt man sich daran, ist „erst denken, dann coden“ oft effizienter.

Tests sind keine Last, sondern Absicherung. Vitest einmal sauber konfigurieren – danach coden Sie ruhiger.

Vitest Unit-Test-Konfiguration und TDD-Workflow

Vollständige Schritte zur Vitest-Konfiguration und praktischer TDD-Entwicklung

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Vitest installieren

    Installationsbefehl ausführen:

    ```bash
    npm install -D vitest
    ```

    Systemvoraussetzungen: Vite >= 6.0.0, Node >= 20.0.0
  2. 2

    Step 2: vite.config.ts konfigurieren

    Test-Feld in der Konfigurationsdatei ergänzen:

    ```typescript
    import { defineConfig } from 'vitest/config'

    export default defineConfig({
    test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    },
    })
    ```

    globals: true spart das Importieren von describe, it und expect.
  3. 3

    Step 3: Run-Skripte hinzufügen

    In package.json ergänzen:

    ```json
    {
    "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
    }
    }
    ```

    npm test startet den Watch-Modus, npm run test:run führt einmal aus (für CI).
  4. 4

    Step 4: Ersten Test schreiben

    Testdatei tests/math.test.ts anlegen:

    ```typescript
    import { describe, it, expect } from 'vitest'

    describe('Math', () => {
    it('should add numbers', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    npm test ausführen, um die Konfiguration zu prüfen.
  5. 5

    Step 5: TDD-Workflow praktizieren

    Testgetriebene Entwicklung befolgen:

    • Schritt 1: Zuerst Test schreiben, erwartetes Verhalten definieren
    • Schritt 2: Minimalen Code implementieren, der besteht
    • Schritt 3: Grenzfälle testen
    • Schritt 4: Refactoring und Optimierung

    Vitest Watch-Modus liefert Feedback in Sekunden.
  6. 6

    Step 6: Coverage konfigurieren

    Coverage-Konfiguration ergänzen:

    ```typescript
    coverage: {
    provider: 'v8',
    reporter: ['text', 'html'],
    thresholds: {
    lines: 80,
    functions: 80,
    },
    }
    ```

    vitest run --coverage ausführen, um den Report zu erzeugen.

FAQ

Was ist der Unterschied zwischen Vitest und Jest?
Vitest ist ein Vite-natives Testframework und teilt die Konfiguration mit Vite. Cold Start etwa 200 ms (Jest: 2–4 Sekunden). Die API ist nahezu vollständig Jest-kompatibel, die Migration ist günstig. Hauptvorteile: Geschwindigkeit, einfache Konfiguration, natives TypeScript.
Wie migriere ich von Jest zu Vitest?
Die Schritte sind einfach:

• Jest-Pakete deinstallieren, vitest installieren
• jest.config.js nach vite.config.ts übertragen
• Imports in Testdateien von 'jest' auf 'vitest' ändern
• jest.fn(), jest.mock() durch vi.fn(), vi.mock() ersetzen

In den meisten Fällen ist die Migration in 30 Minuten erledigt.
Welche Testumgebungen unterstützt Vitest?
Vitest unterstützt mehrere Umgebungen: node (Standard, Backend-Tests), jsdom (Browser-DOM), happy-dom (schnellere DOM-Alternative). Über das environment-Feld konfigurieren; für Browser-Komponenten jsdom installieren.
Wie mocke ich API-Anfragen in Vitest?
Drei gängige Ansätze:

• vi.fn(): Einzelne Funktion mocken, Rückgabewerte vorgeben
• vi.mock(): Ganzes Modul mocken, alle Exports ersetzen
• vi.spyOn(): Echte Funktionsaufrufe überwachen, Implementierung bleibt

Nach jedem Test vi.clearAllMocks() oder vi.restoreAllMocks() aufrufen.
Wie konfiguriere ich Coverage in Vitest?
In vite.config.ts unter test.coverage provider (empfohlen: v8), reporter (text/html/lcov) und thresholds (Schwellenwerte) setzen. vitest run --coverage erzeugt den Report. Unterschreitung der Schwellenwerte führt zu Fehlern – ideal für CI-Qualitätsgates.
Was ist der Kern des TDD-Workflows?
Der Kern von TDD ist der „Rot-Grün-Refactor“-Zyklus:

• Rot: Zuerst einen fehlschlagenden Test schreiben
• Grün: Minimalen Code schreiben, der besteht
• Refactor: Code-Struktur optimieren

Mit Vitest Watch-Modus erhalten Sie nach Codeänderungen Feedback in Sekunden. Tests zuerst zu schreiben, hilft beim Klären des Funktionsdesigns.

8 Min. Lesezeit · Veröffentlicht am: 14. Apr. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog