Advanced (Aktualisiert: 22.7.2026)

Fortgeschrittene Vitest-Tests mit Claude Code

Vitest mit Claude Code: Testdoppel, künstliche Timer, jsdom, Abdeckung, Momentaufnahmen und CI.

Fortgeschrittene Vitest-Tests mit Claude Code

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

BereichAn Claude Code delegierbarMenschliche Freigabe nötig
TestsTestdoppel, künstliche Timer, jsdom-Assertions, AbdeckungsbefehlePrüfen, ob Assertions das Verhalten nicht abschwächen
CIvitest run und vitest run --coverage vorschlagenPflichtchecks, kostenrelevante Runner, Produktion
DatenSynthetische IDs wie ord_1 und book-1 nutzenKundendaten, Secrets, API-Keys, Zahlungs-IDs prüfen
BrowserDOM-Verträge in Vitest haltenReale Abläufe mit Playwright oder manuell freigeben

Fallstricke und Korrekturen

FehlerSymptomKorrektur
Testdoppel nicht zurückgesetztAufrufzahlen oder Fake-Implementierungen laufen ausrestoreMocks, vi.clearAllMocks() oder vi.restoreAllMocks() gezielt nutzen
Künstliche Timer nicht zurückgesetztZeit-Tests scheitern in anderer Dateivi.useRealTimers() in afterEach aufrufen
jsdom als echter Browser verstandenCSS, Layout, Bilder oder Canvas unterscheiden sichDOM-Vertrag in Vitest, Browserverhalten in Playwright
Momentaufnahme zu großReview wird laut und unklarNur kleine Strukturen speichern
coverage.include fehltUngetestete Dateien bleiben unsichtbarsrc/**/*.{ts,tsx} explizit einschließen
Asynchronität nicht erwartetFalsch positive Testsawait expect(promise).resolves oder rejects nutzen
Interaktiver Runner startet den ÜberwachungsmodusProzess wartet auf Änderungenvitest 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.

#Claude Code #Vitest #Tests #TypeScript #Qualitätssicherung
Kostenlos

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.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.