Advanced (Aktualisiert: 3.6.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 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

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
CI läuft im ÜberwachungsmodusJob beendet nichtvitest 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.

#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.