Design wechseln

Vitest Unit-Tests in der Praxis: TDD-Workflow und Coverage-Konfiguration

Easton editorial illustration: failed red test card, passing green test card, central refactor wrench, coverage gate

Sie starren auf die Zeile Test Suites: 1 failed, 47 passed im Terminal. Eine Codezeile ändern, 28 Sekunden warten, bis die Tests durch sind. Noch eine Zeile – wieder 28 Sekunden.

So fühlt sich der Umstieg von Jest auf Vitest oft an.

Damals hatte das Projekt fast 500 Testfälle; jedes npm test reichte für zwei Seiten Hacker News. Nach dem Wechsel zu Vitest liefen dieselben Tests in etwas über 3 Sekunden durch.

Heute geht es um zwei Dinge: wie Sie mit Vitest diese schnelle Test-Erfahrung bekommen, und wie TDD (Test-Driven Development) das Schreiben von Tests weniger mühsam macht. Anhand einer vollständigen Preisformatierungsfunktion führen wir den Red-Green-Refactor-Zyklus durch, besprechen Coverage-Konfiguration, Mock-Techniken und Vitest UI als Debug-Hilfe.

Warum Vitest + TDD

Zuerst ein paar Zahlen.

50.000 Tests
Vitest 3 Sek. vs. Jest 28–34 Sek.

SitePoint verglich 2026: 50.000 Testfälle – Vitest in 3 Sekunden, Jest 28 bis 34 Sekunden. Das ist nicht ein kleiner Unterschied, sondern eine Größenordnung.

Geschwindigkeit ist nur ein Grund. Wer schon ESM mit Jest getestet hat, kennt die Fallstricke – Babel, Transformers, hoffen, dass die „Magie“ in jest.config.js greift. Vitest unterstützt ESM nativ, ohne Transpile-Konfiguration. Code und Tests laufen gleich – klar und direkt.

Für Vite-Nutzer noch ein Plus: Vitest übernimmt die Vite-Konfiguration. Aliase, Umgebungsvariablen, Plugins aus vite.config.ts gelten automatisch in Tests – kein separates moduleNameMapper wie bei Jest. Beim ersten Mal war ich kurz sprachlos: Test-Setup kann so unkompliziert sein?

Zur Methode: TDD kennen viele, konsequent nutzen wenige. Kern ist der Red-Green-Refactor-Zyklus: fehlschlagender Test (rot), minimaler bestandener Code (grün), dann Refactoring. Klingt kontraintuitiv – Tests vor Code?

Der Vorteil: Jede Zeile Code existiert, damit Tests bestehen. Keine Reserve-Logik, kein „für alle Fälle“. Tests zwingen Sie, Funktion, Rückgabe und Grenzen vorher zu klären – das schärft das Design.

Vitest Watch macht den Zyklus flüssig: Speichern, Tests laufen sofort, Ergebnis im Terminal. Kein Fensterwechsel, kein manuelles Kommando – wie ein Co-Pilot: „Hier stimmt was nicht“, „Jetzt alles grün“. Dieses Feedback hält Sie im Flow.

TDD in der Praxis: eine Funktion von null entwickeln

Theorie reicht nicht. Wir entwickeln formatPrice() per TDD – eine Zahl als Währung, z. B. 1234.5¥1,234.50.

Red-Phase: zuerst ein fehlschlagender Test

Neue Datei formatPrice.test.ts:

// formatPrice.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'

describe('formatPrice', () => {
  it('soll Zahlen als CNY-Währungsformat formatieren', () => {
    expect(formatPrice(1234.5)).toBe('¥1,234.50')
  })
})

npx vitest liefert rot: Cannot find module './formatPrice' – die Funktion fehlt noch.

Genau das ist Red: Der Test scheitert und definiert eine noch nicht erfüllte Anforderung. Viele finden Tests zuerst seltsam – wer aber zuerst Code schreibt, weiß nicht sicher, ob der Test wirklich prüft, was gemeint ist.

Green-Phase: minimaler Code zum Bestehen

formatPrice.ts anlegen:

// formatPrice.ts
export function formatPrice(value: number): string {
  return '¥1,234.50'  // zuerst Hardcoding
}

Tests erneut ausführen – grün.

„Ist das nicht Schummeln?“ Nein. TDD verlangt gerade genug Code – Hardcoding oder Minimallogik, solange der Test besteht. Das ist eine verifizierbare Basis; neue Tests treiben die echte Implementierung.

Weiterer Testfall:

it('soll verschiedene Werte korrekt verarbeiten', () => {
  expect(formatPrice(0)).toBe('¥0.00')
  expect(formatPrice(99.99)).toBe('¥99.99')
})

Wieder rot – Hardcoding reicht nicht:

export function formatPrice(value: number): string {
  return `¥${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}

Tests grün. Der Regex ist unschön – für jetzt reicht es.

Refactor-Phase: Struktur verbessern

Tests bestehen, der Code kann klarer werden. Refactoring ist sicher – Tests fangen Fehler sofort ab.

// Refactorierte Version
export function formatPrice(value: number): string {
  // Intl.NumberFormat ist robuster
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY',
    minimumFractionDigits: 2,
  }).format(value)
}

Tests erneut – weiterhin grün. Refactoring abgeschlossen.

So läuft ein Red-Green-Refactor-Durchlauf: von rot über Minimalcode zu besserer Struktur, immer abgesichert. Kleine Schritte, geringe mentale Last – Grenzfälle kommen durch Tests, nicht durch Vorab-Planung.

In Projekten nutze ich meist Watch: speichern → Tests laufen → Ergebnis → anpassen → speichern. Sekunden, ohne den Editor zu verlassen. „Sofort wissen, ob es stimmt“ – das macht Spaß.

Coverage-Konfiguration und CI-Integration

Tests geschrieben – wie viel Code decken sie ab? Dafür gibt es Coverage-Reports.

Grundkonfiguration

In vitest.config.ts:

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

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',      // oder 'istanbul' – v8 ist meist schneller
      reporter: ['text', 'html', 'json-summary'],
      reportsDirectory: './coverage',
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/types/**'],
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80,
      },
    },
  },
})

Zwei Provider: v8 (native V8-Coverage-API, schneller) und istanbul (bewährt, breitere Kompatibilität). Reines Vite/Node – v8 reicht.

Reporter: text ins Terminal, html visualisiert, json-summary für CI-Tools.

Schwellenwerte

thresholds hat vier Dimensionen:

  • statements: Anteil ausgeführter Anweisungen
  • branches: if/else-Zweige getestet?
  • functions: wie viele Funktionen aufgerufen wurden
  • lines: ähnlich statements, andere Berechnung

Ich setze meist 75–85 %. Zu niedrig nutzlos, zu hoch ermüdet – Randfälle und Fehlerbehandlung sind oft schwer auf 100 % zu bringen.

GitHub Actions

Coverage lohnt sich besonders, wenn CI PRs unter Schwellen blockiert. In .github/workflows/test.yml:

- name: Run tests with coverage
  run: npm run test -- --coverage

- name: Check coverage threshold
  run: |
    COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      echo "Coverage $COVERAGE% is below threshold 80%"
      exit 1
    fi

Unter 80 % Lines – kein Merge. Das zwingt zum ausreichenden Testen vor dem Push.

Report lesen

Nach npx vitest --coverage etwa:

 % Stmts   % Branch   % Funcs   % Lines   Uncovered Line
----------|----------|----------|----------|----------------
  82.45    |   76.32   |   85.71   |   82.45   | 23-25, 67

Uncovered Line listet ungetestete Zeilen. coverage/index.html zeigt Grün (getestet) und Rot (offen).

Am Anfang wollte ich alles voll decken – unnötig. 80 % trifft meist Kernlogik und Hauptzweige; der Rest sind oft extreme Randfälle.

Mock-Dreiklang: vi.fn, vi.spy, vi.mock

Ärgerlich sind externe Abhängigkeiten – API, Timer, Libraries. Vitest bietet drei Mock-Wege.

vi.fn(): Fake-Funktion

Wenn Sie eine Funktion brauchen, die nur Aufrufe protokolliert:

test('Callback soll einmal aufgerufen werden', () => {
  const callback = vi.fn()

  callMeMaybe(callback)

  expect(callback).toHaveBeenCalledTimes(1)
  expect(callback).toHaveBeenCalledWith('hello')
})

callback ist neu, protokolliert Aufrufe, Parameter, Rückgaben – per mockReturnValue oder mockImplementation steuerbar.

vi.spy(): echte Funktion beobachten

Funktion behalten, aber mitverfolgen – vi.spyOn:

test('soll console.log aufrufen', () => {
  const logSpy = vi.spyOn(console, 'log')

  greet('World')

  expect(logSpy).toHaveBeenCalledWith('Hello, World!')
  logSpy.mockRestore()  // nicht vergessen
})

Spy behält Verhalten, hört mit. Danach mockRestore(), sonst beeinflussen andere Tests.

vi.mock(): ganzes Modul ersetzen

API-Antworten oder Libraries simulieren:

// axios mocken
vi.mock('axios', () => ({
  default: {
    get: vi.fn(() => Promise.resolve({ data: { name: 'test' } }))
  }
}))

test('getUser soll Nutzerdaten zurückgeben', async () => {
  const user = await getUser(1)

  expect(user.name).toBe('test')
  expect(axios.get).toHaveBeenCalledWith('/users/1')
})

vi.mock wird an den Dateianfang gehoisted – Mock-Inhalt darf nicht von anderen Variablen abhängen.

Welches wählen?

Kurz:

  • Nur Fake-Funktion? → vi.fn()
  • Echte Funktion beobachten? → vi.spy()
  • Modul ersetzen? → vi.mock()

Merksatz: fn fälscht, spy schaut zu, mock tauscht alles aus.

Aufräumen

Tests dürfen sich nicht gegenseitig stören – Mocks nach jedem Test zurücksetzen:

afterEach(() => {
  vi.restoreAllMocks()
})

Oder global in der Config:

test: {
  restoreMocks: true
}

Vitest UI und Debug-Tipps

Terminal reicht oft – für mehr Überblick: Vitest UI.

UI starten

npx vitest --ui

Browser mit Testliste links, Details rechts. Klick auf einen Test zeigt Output, Stack, Laufzeit. Coverage-Button öffnet den HTML-Report.

Ideal zum Debuggen: Fehler ohne Terminal-Scroll, Code daneben, Speichern → UI aktualisiert.

Watch: nur Betroffenes

Im Alltag: npx vitest im Watch-Modus. Geändert wird utils/formatPrice.ts – nur zugehörige Tests laufen, kein Full Run.

Bei 800+ Tests: Full Run ~4 Sekunden, inkrementell oft nur wenige hundert Millisekunden.

Debug-Tricks

Einzeltest: .only an it

it.only('dieser Test hängt, isoliert laufen', () => {
  // ...
})

Überspringen: .skip

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

Snapshots aktualisieren:

npx vitest -u  # Kurzform von --update

console.log: alt, aber wirksam – Vitest zeigt Ausgabe im Testergebnis.

Häufige Fehler

FehlerUrsacheLösung
Cannot find moduleAlias falschalias in vitest.config prüfen
vi.mock is not a functionfalscher Importimport { vi } from 'vitest'
falsche ZeitzoneStandard UTCin setup process.env.TZ = 'Asia/Shanghai'

Besonders die Zeitzone: lokal grün, CI rot – oft erst nach stundenlangem Suchen klar.

Fazit

Kurz gesagt: Vitest + TDD macht Tests weniger unangenehm.

Von Jest-zehntelsekunden auf wenige Sekunden – das ändert die Erfahrung: kein Pendeln zwischen Code und Wartezeit. ESM nativ, Vite-Config übernommen – spürbar weniger Aufwand.

Red-Green-Refactor wirkt kontraintuitiv, lohnt sich aber: kleine Schritte, ständige Verifikation, leichter Kopf. Kein perfektes Design upfront – Tests führen durch Iterationen.

Coverage und Mocks sind Werkzeuge für robustere Tests. Wichtiger ist die Gewohnheit – nicht für Zahlen, sondern für Vertrauen in den Code.

Läuft Ihr Projekt schon auf Vite, ist Migration von Jest günstig: npm add -D vitest, Syntax fast wie Jest, loslegen. Unsicher? Ein kleines Modul, Watch-Modus ausprobieren.

Starten Sie jetzt den ersten Vitest-Test und den TDD-Zyklus – spüren Sie, wie gut „sofort wissen, ob es stimmt“ ist. Vielleicht mögen Sie Tests danach genauso wie ich.

Vitest-TDD-Praxisworkflow

Eine Funktion mit TDD entwickeln und Coverage-Reports konfigurieren

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Vitest installieren

    Vitest in einem Vite-Projekt hinzufügen:

    npm add -D vitest

    Keine Extra-Konfiguration nötig – Vitest nutzt die Vite-Konfiguration automatisch
  2. 2

    Step 2: Red-Phase: fehlschlagenden Test schreiben

    Testdatei anlegen und einen Test schreiben, der garantiert fehlschlägt:

    import { describe, it, expect } from 'vitest'
    import { formatPrice } from './formatPrice'

    describe('formatPrice', () => {
    it('soll Währung formatieren', () => {
    expect(formatPrice(1234.5)).toBe('¥1,234.50')
    })
    })

    npx vitest ausführen und prüfen, dass der Test fehlschlägt (rot)
  3. 3

    Step 3: Green-Phase: minimalen Code schreiben

    Implementierungsdatei anlegen und gerade genug Code schreiben:

    export function formatPrice(value: number): string {
    return '¥1,234.50' // zuerst hardcoden
    }

    Tests ausführen und grün bestätigen
  4. 4

    Step 4: Refactor-Phase: Code optimieren

    Mit Intl.NumberFormat refaktorisieren:

    export function formatPrice(value: number): string {
    return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY',
    }).format(value)
    }

    Prüfen, dass die Tests weiterhin bestehen
  5. 5

    Step 5: Coverage konfigurieren

    In vitest.config.ts ergänzen:

    test: {
    coverage: {
    provider: 'v8',
    thresholds: { statements: 80, branches: 75 }
    }
    }

    npx vitest --coverage ausführen und den Report ansehen

FAQ

Was sind die Hauptunterschiede zwischen Vitest und Jest?
Vitest ist für Vite gebaut, unterstützt ESM nativ und ist 5–10× schneller als Jest. Vitest übernimmt Vite-Konfiguration (Aliase, Umgebungsvariablen) automatisch – kein moduleNameMapper wie bei Jest nötig.
Wie funktioniert der Red-Green-Refactor-Zyklus bei TDD?
Drei Schritte im Loop:

• Red: zuerst einen fehlschlagenden Test schreiben, Anforderung definieren
• Green: minimalen Code schreiben, der besteht – Hardcoding ist erlaubt
• Refactor: unter Testschutz Struktur verbessern

Jeder Schritt bleibt klein, die kognitive Last gering – Tests sichern alles ab.
Welche Coverage-Schwellenwerte sind sinnvoll?
Empfehlung: 75–85 %. Zu niedrig bringt wenig, zu hoch kostet Zeit. 80 % deckt meist die Kernlogik ab; die restlichen 20 % sind oft extreme Randfälle, die sich kaum lohnen.
Wann vi.fn, vi.spy oder vi.mock?
Je nach Szenario:

• vi.fn(): komplett neue Fake-Funktion, Aufrufe und Parameter protokollieren
• vi.spy(): echte Funktion überwachen, Verhalten bleibt erhalten, danach mockRestore()
• vi.mock(): ganzes Modul ersetzen, z. B. API oder Drittanbieter

Merksatz: fn fälscht, spy schaut zu, mock tauscht alles aus.
Was bringt der Vitest-Watch-Modus?
Inkrementelle Tests laufen nur betroffene Dateien – 800 Tests vollständig in 4 Sekunden, inkrementell oft nur wenige hundert Millisekunden. Nach dem Speichern sofort Ergebnis, ohne Fensterwechsel – ideal für Flow.
Wie löse ich Zeitzonenprobleme in Tests?
Vitest nutzt standardmäßig UTC. In vitest.config.ts unter setupFiles process.env.TZ = 'Asia/Shanghai' setzen oder am Anfang der Testdatei manuell konfigurieren.

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

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog