Fortgeschrittene Vitest-Tests mit Claude Code
Vitest mit Claude Code: Testdoppel, künstliche Timer, jsdom, Abdeckung, Momentaufnahmen und CI.
Welches Problem dieser Vitest-Workflow löst
Wenn Claude Code nur den Auftrag “füge Vitest-Tests hinzu” bekommt, entstehen oft Tests, die lokal grün sind, aber bei Zeitlogik, DOM, externen API-Grenzen oder CI scheitern. Dieser Leitfaden bündelt die kritischen Stellen in einen klaren Workflow: Testdoppel für fremde Abhängigkeiten, künstliche Timer für kontrollierte Zeit, Abdeckung für ungetestete Zweige, jsdom für DOM-Struktur, kleine Momentaufnahmen für Rendering-Verträge und CI-Befehle, die zuverlässig beenden.
Am 22. Juli 2026 wurde der Artikel mit der offiziellen Vitest-CLI-Anleitung, der watch-Konfiguration und dem Vitest-4-Migrationsleitfaden abgeglichen. Vitest 4 benötigt Vite 6 oder neuer und Node 20 oder neuer. In CI oder nicht interaktiven Terminals wechselt vitest automatisch in den einmaligen Lauf; vitest run macht die Absicht im Skript trotzdem eindeutig.
Gib Claude Code nicht nur das Ziel, sondern auch die Testgrenze: Was wird ersetzt, welche Uhr wird fixiert, reicht jsdom, und welcher Befehl beweist das Ergebnis? Ergänzend passen Claude Code Teststrategien, MSW API-Mocks und Playwright E2E-Tests.
flowchart TD
A["Spezifikation: Erfolg und Fehler"] --> B["Vitest config: node/jsdom/coverage"]
B --> C["Unit-Tests: Logik und API-Grenzen"]
B --> D["Zeit: künstliche Timer und feste Date"]
B --> E["DOM: jsdom und Momentaufnahmen"]
C --> F["CI: vitest run --coverage"]
D --> F
E --> F
Mit stabiler Konfiguration starten
Installiere Vitest, den V8-Abdeckungsanbieter, jsdom und TypeScript. Eine Vite-App kann Konfiguration teilen, aber ein eigenes vitest.config.ts macht die Absicht für Claude Code und Reviews klarer.
npm install -D vitest @vitest/coverage-v8 jsdom typescript
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: false,
restoreMocks: true,
coverage: {
provider: "v8",
reporter: ["text", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.d.ts", "src/**/*.test.{ts,tsx}", "src/test/**"],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
},
});
globals: false hält Imports sichtbar. Das hilft, wenn Claude Code Tests zwischen Dateien verschiebt. restoreMocks: true reduziert auslaufende Testdoppel, ersetzt aber nicht das Zurücksetzen von künstlichen Timern oder DOM.
Anwendungsfall 1: API-Grenzen ersetzen
Ein Unit-Test sollte keine echte Bestell-, Zahlungs- oder Nutzer-API aufrufen. Teste den Vertrag, den du besitzt: Pfad, Nutzlast, Eingabevalidierung und Fehlerübersetzung.
// src/orders.ts
export type ApiClient = {
post<T>(path: string, body: unknown): Promise<T>;
};
export class OrderError extends Error {
constructor(message = "Order request failed") {
super(message);
this.name = "OrderError";
}
}
type OrderInput = {
sku: string;
quantity: number;
};
type OrderResponse = {
id: string;
status: "accepted" | "queued";
};
export async function createOrder(api: ApiClient, input: OrderInput) {
if (input.quantity < 1) {
throw new OrderError("Quantity must be at least 1");
}
try {
return await api.post<OrderResponse>("/orders", input);
} catch {
throw new OrderError("Order API failed");
}
}
// src/orders.test.ts
import { describe, expect, it, vi } from "vitest";
import { createOrder, type ApiClient, OrderError } from "./orders";
describe("createOrder", () => {
it("posts the order payload to the API", async () => {
const api: ApiClient = {
post: vi.fn().mockResolvedValue({ id: "ord_1", status: "accepted" }),
};
await expect(createOrder(api, { sku: "book-1", quantity: 2 })).resolves.toEqual({
id: "ord_1",
status: "accepted",
});
expect(api.post).toHaveBeenCalledWith("/orders", { sku: "book-1", quantity: 2 });
});
it("rejects invalid quantity before calling the API", async () => {
const api: ApiClient = { post: vi.fn() };
await expect(createOrder(api, { sku: "book-1", quantity: 0 })).rejects.toBeInstanceOf(
OrderError,
);
expect(api.post).not.toHaveBeenCalled();
});
it("wraps transport errors in a domain error", async () => {
const api: ApiClient = {
post: vi.fn().mockRejectedValue(new Error("ECONNRESET")),
};
await expect(createOrder(api, { sku: "book-1", quantity: 1 })).rejects.toThrow(
"Order API failed",
);
});
});
Diese Abhängigkeitsübergabe ist oft klarer als ein kompletter Modul-Mock. vi.mock() ist nützlich, wird aber vor Imports gehoben. Ein falscher Aufbau kann für Einsteiger und generierten Code schwer nachvollziehbar sein.
Anwendungsfall 2: Zeit mit künstlichen Timern fixieren
Testversionen, Wiederholungen, Benachrichtigungen und Entprellung werden instabil, wenn Tests echte Zeit abwarten. Vitest kann setTimeout, setInterval und die Systemzeit kontrollieren.
// src/trial.ts
const DAY_MS = 24 * 60 * 60 * 1000;
export function getTrialEndsAt(days = 7) {
return new Date(Date.now() + days * DAY_MS).toISOString();
}
export function scheduleTrialReminder(send: () => void, days = 7) {
return setTimeout(send, days * DAY_MS);
}
// src/trial.test.ts
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getTrialEndsAt, scheduleTrialReminder } from "./trial";
describe("trial reminder", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-03T00:00:00.000Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("calculates the trial end date from the fixed clock", () => {
expect(getTrialEndsAt()).toBe("2026-06-10T00:00:00.000Z");
});
it("runs the reminder after the configured number of days", () => {
const send = vi.fn();
const timer = scheduleTrialReminder(send, 3);
vi.advanceTimersByTime(3 * 24 * 60 * 60 * 1000 - 1);
expect(send).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(send).toHaveBeenCalledTimes(1);
clearTimeout(timer);
});
});
Der häufigste Fehler ist ein fehlendes vi.useRealTimers(). Bleibt die falsche Uhr aktiv, kann ein anderer Test zufällig scheitern. Bei Promises muss zusätzlich await gesetzt werden. Zeit- und Zeitzonengrenzen behandelt Datum und Zeit mit Claude Code.
Anwendungsfall 3: DOM mit jsdom und Momentaufnahmen sichern
jsdom ahmt DOM-APIs in Node nach. Es eignet sich für Struktur, Text und Barrierefreiheitsattribute. Es ersetzt keinen echten Browser für Layout, Fokus, Canvas oder visuelle Regression.
// src/notice.ts
export function renderNotice(target: HTMLElement, message: string) {
target.innerHTML = "";
const notice = document.createElement("p");
notice.setAttribute("role", "status");
notice.dataset.testid = "notice";
notice.textContent = message;
target.append(notice);
return notice;
}
// src/notice.test.ts
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { renderNotice } from "./notice";
afterEach(() => {
document.body.innerHTML = "";
});
describe("renderNotice", () => {
it("renders an accessible status message", () => {
document.body.innerHTML = '<div id="app"></div>';
const target = document.querySelector<HTMLDivElement>("#app");
if (!target) throw new Error("missing #app");
const notice = renderNotice(target, "Gespeichert");
expect(notice.getAttribute("role")).toBe("status");
expect(notice.textContent).toBe("Gespeichert");
expect({
html: document.body.innerHTML,
text: notice.textContent,
}).toMatchInlineSnapshot(`
{
"html": "<div id=\\"app\\"><p role=\\"status\\" data-testid=\\"notice\\">Gespeichert</p></div>",
"text": "Gespeichert",
}
`);
});
});
Momentaufnahmen sollten klein bleiben. Wichtige Attribute prüfst du direkt mit expect; die Momentaufnahme speichert nur eine kompakte Struktur. Browserverhalten gehört in Playwright.
Abdeckung und CI
Abdeckung soll ungetestete Zweige sichtbar machen, nicht nur Prozentwerte erhöhen. Vitest dokumentiert V8 und Istanbul als Anbieter, V8 ist der Standard. Setze coverage.include, sonst können nie importierte Dateien aus dem Bericht verschwinden.
# .github/workflows/vitest.yml
name: vitest
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:run
- run: npm run coverage
Nutze in CI ausdrücklich vitest run. vitest überwacht nur ohne CI in einem interaktiven Terminal; in CI oder nicht interaktiven Terminals läuft es einmal. Für interaktive Werkzeuge wie lint-staged eignet sich vitest related --run. Den größeren Ablauf erklärt Claude Code CI/CD einrichten.
Praktischer Prompt für Claude Code
Füge Vitest-Tests für src/orders.ts hinzu.
Teste nur createOrder.
Ersetze die externe API mit vi.fn(); keine echten HTTP-Aufrufe.
Enthalten sein müssen Erfolg, ungültige Eingabe und Transportfehler.
Nutze künstliche Timer oder jsdom nur, wenn der Code sie braucht.
Berichte danach den erwarteten Befehl npm run test:run und verbleibende Risiken.
Dieser Prompt gibt Umfang, Ersatzgrenze, Fehlerfälle und Nachweisbefehl vor. Lege dieselbe Regel in CLAUDE.md Best Practices ab.
Drei Anwendungsfälle
Anwendungsfall 1 ist ein Bestell- oder Registrierungsformular. Ein falscher Pfad oder Body kann Umsatz kosten. Claude Code kann mit vi.fn() Tests für Erfolg, ungültige Eingabe und Transportfehler entwerfen; Zahlung, Abrechnung, Bestand und Kundendatenregeln müssen Menschen freigeben.
Anwendungsfall 2 sind Erinnerungen für Testphase, Termin oder Verlängerung. Claude Code kann Date fixieren, künstliche Timer fortschalten und zeigen, dass der Callback genau einmal läuft. Zeitzonen, Werktage, echte E-Mail- oder SMS-Sendung und Abrechnungsfolgen prüft ein Mensch.
Anwendungsfall 3 sind kleine DOM-Verträge wie gespeicherte Meldung, Validierung oder Admin-Banner. Claude Code kann Text, Rollen und kleine Momentaufnahmen mit jsdom prüfen. Echtes Browserverhalten, Fokus, visuelle Regression und Kundentexte brauchen menschliche Freigabe.
Was Claude Code erledigt und was Menschen freigeben
| Bereich | An Claude Code delegierbar | Menschliche Freigabe nötig |
|---|---|---|
| Tests | Testdoppel, künstliche Timer, jsdom-Assertions, Abdeckungsbefehle | Prüfen, ob Assertions das Verhalten nicht abschwächen |
| CI | vitest run und vitest run --coverage vorschlagen | Pflichtchecks, kostenrelevante Runner, Produktion |
| Daten | Synthetische IDs wie ord_1 und book-1 nutzen | Kundendaten, Secrets, API-Keys, Zahlungs-IDs prüfen |
| Browser | DOM-Verträge in Vitest halten | Reale Abläufe mit Playwright oder manuell freigeben |
Fallstricke und Korrekturen
| Fehler | Symptom | Korrektur |
|---|---|---|
| Testdoppel nicht zurückgesetzt | Aufrufzahlen oder Fake-Implementierungen laufen aus | restoreMocks, vi.clearAllMocks() oder vi.restoreAllMocks() gezielt nutzen |
| Künstliche Timer nicht zurückgesetzt | Zeit-Tests scheitern in anderer Datei | vi.useRealTimers() in afterEach aufrufen |
| jsdom als echter Browser verstanden | CSS, Layout, Bilder oder Canvas unterscheiden sich | DOM-Vertrag in Vitest, Browserverhalten in Playwright |
| Momentaufnahme zu groß | Review wird laut und unklar | Nur kleine Strukturen speichern |
coverage.include fehlt | Ungetestete Dateien bleiben unsichtbar | src/**/*.{ts,tsx} explizit einschließen |
| Asynchronität nicht erwartet | Falsch positive Tests | await expect(promise).resolves oder rejects nutzen |
| Interaktiver Runner startet den Überwachungsmodus | Prozess wartet auf Änderungen | vitest run; für geänderte Dateien vitest related --run nutzen |
Die Korrektur heißt nicht, Tests um jeden Preis grün zu machen. Wenn ein Test scheitert, füge den vollständigen Fehler ein, verbiete das Entfernen von Assertions und fordere zuerst eine Korrektur in Implementierung oder Mock-Setup. Sicherheit, Produktion, Abrechnung und Kundendaten bleiben bis zur menschlichen Diff-Prüfung außerhalb automatischer Änderungen.
Für dieselben Review-Regeln im eigenen Repository bietet ClaudeCodeLab praktische Vorlagen für Teststandards, Review-Prompts und CI-Gates.
Verifiziertes Ergebnis
Am 22. Juli 2026 wurden die TypeScript-Beispiele in eine temporäre Testumgebung extrahiert und mit Node v24.14.1 sowie Vitest 4.1.10 ausgeführt. vitest run und vitest run --coverage bestanden mit 4 Dateien und 7 Tests für API-Grenze, Modul-Mock, feste Uhr, Timer-Grenze, DOM-Attribute und Inline-Snapshot. Die Abdeckung betrug 94,73% Statements, 100% Branches, 85,71% Functions und 94,73% Lines. Der Mailer war gemockt; echte E-Mails oder Produktions-APIs wurden nicht aufgerufen.
Ähnliche Artikel
E2E-Tests mit Claude Code und Playwright produktionsreif einführen
Plane Playwright-E2E mit Claude Code: Mobile Screenshots, Auth State, Trace Viewer, Selektoren und CI-Retries.
Claude Agent SDK: Claude Code sicher in Apps einbetten
Aktuelles Claude Agent SDK Setup mit Permissions, MCP, Codebeispielen und Produktionsfallen.
Teststrategie mit Claude Code: Vitest, Testing Library, Playwright und CI
Praktische Teststrategie mit Claude Code für Unit-, Integrations-, E2E- und CI-Tests.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.