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 3. Juni 2026 habe ich die offiziellen Vitest-Dokumente geprüft: Getting Started, Mocking, Timers, Dates, Test Environment, Coverage, Snapshot und CLI. Die Dokumentation zu Vitest 4 unterscheidet Überwachungsmodus und vitest run; in CI ist diese Unterscheidung entscheidend.
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 vitest run. Der Befehl vitest allein kann in den Überwachungsmodus gehen. 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.
Häufige Fehler
| 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 |
| CI läuft im Überwachungsmodus | Job beendet nicht | vitest run oder vitest related --run nutzen |
Wenn du diesen Workflow an dein Repository anpassen möchtest, bietet ClaudeCodeLab eine englische Trainingsseite und praktische Vorlagen für Teststandards, Review-Prompts und CI-Gates.
Verifiziertes Ergebnis
Das Ergebnis ist eine kleine Vitest-Basis mit drei kopierbaren Fällen: API-Grenze, feste Zeit und jsdom-Rendering mit kleiner Momentaufnahme. Vor der Veröffentlichung habe ich Vitest-Dokumente, interne Links, externe Links, Codeblöcke, updatedDate, Abdeckungskonfiguration und vitest run in CI geprüft.
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.
Ähnliche Artikel
Claude-Code-Permission-Receipt: Scope, Beweis und Rollback festhalten
Permission-Receipt für Claude Code: erlaubte Aktionen, Freigabegrenzen, Prüfbefehle, Rollback und Umsatz-CTA-Prüfung.
Claude Code oder Codex – was denn nun? Die realistische Antwort: beide, ohne Unfall
OpenAIs Codex und Claude Code – wer kann was, wem überlässt man was? Die Aufgabenteilung und der Workflow aus Rechten und Verifikation
Claude Code Subagents: Praxisleitfaden für sichere Agent-Delegation
Claude Code Subagents praktisch nutzen: Artikel- und Codearbeit sicher aufteilen, Prompts einsetzen, Fehler vermeiden.