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

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.
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
| Fehler | Ursache | Lösung |
|---|---|---|
Cannot find module | Alias falsch | alias in vitest.config prüfen |
vi.mock is not a function | falscher Import | import { vi } from 'vitest' |
| falsche Zeitzone | Standard UTC | in 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
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
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
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
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
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?
Wie funktioniert der Red-Green-Refactor-Zyklus bei TDD?
• 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?
Wann vi.fn, vi.spy oder vi.mock?
• 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?
Wie löse ich Zeitzonenprobleme in Tests?
7 Min. Lesezeit · Veröffentlicht am: 29. Apr. 2026 · Aktualisiert am: 14. Juli 2026
Vitest Testing Guide
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Vitest Unit-Tests in der Praxis: Von der Konfiguration zum TDD-Workflow
Vitest Unit-Tests von Grund auf: Installation, TDD-Workflow, fortgeschrittenes Mocking und Coverage-Konfiguration. Vitest ist bis zu 10× schneller als Jest, API-kompatibel – Migration in 30 Minuten.
Teil 1 von 3
Nächster
Vitest Komponententests in der Praxis: Browser Mode und Playwright-Integration
Praxisleitfaden zu Vitest Browser Mode: Playwright-Integration konfigurieren, React/Vue-Komponententests schreiben, CI-Coverage-Gates einrichten und jsdom vs. echten Browser-Tests vergleichen.
Teil 3 von 3
Ähnliche Beiträge
Self-Hosted Dev Sandboxes mit Docker und Go: Preview-URLs ohne Kubernetes

Self-Hosted Dev Sandboxes mit Docker und Go: Preview-URLs ohne Kubernetes
Cloudflare Pro oder Business? Dreidimensionaler Entscheidungsbaum für den Upgrade-Zeitpunkt


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen