Testing & QA

Jest Testing mit Claude Code: Unit & Integration Tests 2026

Jest 30, TypeScript, Testing Library: Wie Claude Code vollständige Test-Suiten schreibt — von einfachen Unit Tests bis zu vollständiger CI-Coverage.

📅 6. Mai 2026 ⏰ 11 min Lesezeit 📋 Jest 30 · TypeScript · CI/CD
← Zurück zum Blog
Jest 30 TypeScript Unit Tests Integration Tests Mocking React Testing Library Coverage GitHub Actions Claude Code

Automatisierte Tests sind 2026 kein Nice-to-have mehr — sie sind das Fundament jedes professionellen TypeScript-Projekts. Jest 30 hat mit nativem ESM-Support, stark verbesserter Leistung und einer überarbeiteten Konfigurations-API einen neuen Standard gesetzt. Claude Code beschleunigt das Schreiben vollständiger Test-Suiten dramatisch: von der ersten jest.config.ts bis zum letzten Coverage-Bericht.

Dieser Artikel zeigt praxisnahe Muster für alle wichtigen Jest-Bereiche — mit echtem TypeScript-Code, den Claude Code täglich generiert.

Voraussetzungen: Node.js 20+, TypeScript 5.x, ein bestehendes npm/pnpm-Projekt. Alle Codebeispiele sind direkt aus realen Claude-Code-Sessions entnommen.

1. Jest Setup & Grundlagen

Ein sauberes Jest-Setup mit TypeScript beginnt mit der richtigen Konfiguration. Claude Code legt immer jest.config.ts statt jest.config.js an — das bringt vollständige Typsicherheit in der Konfiguration selbst.

Installation & jest.config.ts

# Installation (pnpm) pnpm add -D jest ts-jest @types/jest pnpm add -D jest-environment-jsdom # für Browser-Tests
// jest.config.ts import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: [ '**/__tests__/**/*.test.ts', '**/*.spec.ts', ], transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json', }], }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/*.spec.ts', ], coverageThreshold: { global: { branches: 80, functions: 85, lines: 85, statements: 85, }, }, }; export default config;

💡 Claude Code Tipp: tsconfig.jest.json

Claude Code erstellt immer eine separate tsconfig.jest.json — das verhindert Konflikte mit stricten Build-Einstellungen:

{ "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", "types": ["jest", "node"] } }

describe / it / expect — Grundstruktur

Claude Code folgt immer dem AAA-Muster: Arrange, Act, Assert. Das macht Tests lesbar und wartbar.

// src/__tests__/userService.test.ts import { createUser, validateEmail } from '@/services/userService'; import type { User } from '@/types'; describe('UserService', () => { describe('validateEmail', () => { it('akzeptiert gültige E-Mail-Adressen', () => { // Arrange const validEmails = [ 'user@example.com', 'test.user+tag@domain.de', 'admin@sub.domain.io', ]; // Act & Assert validEmails.forEach(email => { expect(validateEmail(email)).toBe(true); }); }); it('lehnt ungültige E-Mail-Adressen ab', () => { expect(validateEmail('kein-at-zeichen')).toBe(false); expect(validateEmail('')).toBe(false); expect(validateEmail('@domain.com')).toBe(false); }); }); describe('createUser', () => { let testUser: Partial<User>; beforeEach(() => { testUser = { email: 'test@example.com', name: 'Max Mustermann', role: 'user', }; }); afterAll(() => { // Cleanup-Logik nach allen Tests in dieser Suite console.log('UserService Tests abgeschlossen'); }); it('erstellt einen neuen Benutzer mit korrekten Defaults', async () => { const user = await createUser(testUser); expect(user).toMatchObject({ email: 'test@example.com', name: 'Max Mustermann', role: 'user', isActive: true, }); expect(user.id).toBeDefined(); expect(user.createdAt).toBeInstanceOf(Date); }); }); });

Wichtige Matcher im Überblick

Matcher Verwendung Beispiel
toBe Exakter Vergleich (===) expect(2 + 2).toBe(4)
toEqual Tiefer Objekt-Vergleich expect(obj).toEqual({a: 1})
toMatchObject Partieller Objekt-Match expect(user).toMatchObject({role: 'admin'})
toThrow Fehler-Erwartung expect(() => fn()).toThrow('Error')
resolves / rejects Promise-Assertions await expect(p).resolves.toBe(42)
toHaveBeenCalledWith Mock-Aufruf prüfen expect(mockFn).toHaveBeenCalledWith('arg')

2. Mocking: jest.mock, jest.fn & jest.spyOn

Mock Spy

Mocking ist das Herzstück von Unit Tests. Claude Code wählt dabei immer das richtige Werkzeug: jest.mock für ganze Module, jest.fn für einzelne Funktionen und jest.spyOn wenn das originale Verhalten erhalten bleiben soll.

jest.mock — Ganzes Modul ersetzen

// src/__tests__/emailService.test.ts import { sendWelcomeEmail } from '@/services/emailService'; import * as nodemailer from 'nodemailer'; // Gesamtes Modul mocken — BEVOR Imports evaluiert werden jest.mock('nodemailer'); const mockSendMail = jest.fn(); const mockCreateTransport = nodemailer.createTransport as jest.Mock; beforeEach(() => { mockCreateTransport.mockReturnValue({ sendMail: mockSendMail, }); }); afterEach(() => { jest.clearAllMocks(); }); it('sendet Willkommens-E-Mail mit korrekten Daten', async () => { mockSendMail.mockResolvedValue({ messageId: 'msg-123' }); await sendWelcomeEmail('user@example.com', 'Max'); expect(mockSendMail).toHaveBeenCalledTimes(1); expect(mockSendMail).toHaveBeenCalledWith(expect.objectContaining({ to: 'user@example.com', subject: expect.stringContaining('Willkommen'), })); });

jest.fn — Präzise Mock-Funktionen

// Verschiedene Rückgabewerte je Aufruf const mockFetch = jest.fn() .mockResolvedValueOnce({ status: 200, data: { id: 1 } }) .mockResolvedValueOnce({ status: 200, data: { id: 2 } }) .mockRejectedValueOnce(new Error('Netzwerkfehler')); // Typ-sichere Mock-Implementierung const mockUserRepo = { findById: jest.fn<Promise<User>, [string]>(), save: jest.fn<Promise<void>, [User]>(), delete: jest.fn<Promise<boolean>, [string]>(), }; it('ruft findById mit korrekter ID auf', async () => { mockUserRepo.findById.mockResolvedValue({ id: 'u-123', email: 'test@test.de', name: 'Test User', } as User); const service = new UserService(mockUserRepo); await service.getUser('u-123'); expect(mockUserRepo.findById).toHaveBeenCalledWith('u-123'); });

jest.spyOn — Original behalten, Aufrufe tracken

// spyOn: Original-Implementierung bleibt erhalten it('loggt Fehler bei fehlgeschlagener Validierung', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); validateInput({ email: 'ungueltig' }); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('Validierungsfehler') ); consoleSpy.mockRestore(); // WICHTIG: Original wiederherstellen! }); // spyOn auf Klassen-Methoden it('delegiert an externen Dienst', async () => { const service = new PaymentService(); const chargeSpy = jest .spyOn(service, 'chargeCard') .mockResolvedValue({ success: true, transactionId: 'tx-999' }); await service.processPayment(49.99, 'EUR'); expect(chargeSpy).toHaveBeenCalledWith(49.99, 'EUR'); expect(chargeSpy).toHaveBeenCalledTimes(1); });

Claude Code Faustregeln: jest.mock() für externe Dependencies (Datenbank, HTTP, Filesystem). jest.fn() für Dependency Injection. jest.spyOn() wenn du sicherstellen willst, dass echte Implementierung in anderen Tests weiterläuft.

3. Timer & Module Mocks

Timer Module

Zeitabhängige Tests — Debounce, Retry-Logik, Timeouts — sind ohne Fake-Timers extrem fragil. Claude Code setzt jest.useFakeTimers konsequent für alle zeitabhängigen Tests ein.

jest.useFakeTimers

// src/__tests__/debounce.test.ts import { debounce } from '@/utils/debounce'; describe('debounce', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); // IMMER zurücksetzen! }); it('führt Funktion erst nach Wartezeit aus', () => { const callback = jest.fn(); const debouncedFn = debounce(callback, 500); debouncedFn('erster Aufruf'); debouncedFn('zweiter Aufruf'); debouncedFn('dritter Aufruf'); // Noch nicht ausgeführt expect(callback).not.toHaveBeenCalled(); // Timer um 499ms vorspulen — noch immer nicht! jest.advanceTimersByTime(499); expect(callback).not.toHaveBeenCalled(); // Exakt 500ms — jetzt wird's ausgeführt jest.advanceTimersByTime(1); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith('dritter Aufruf'); }); it('Retry-Logik mit exponential backoff', async () => { const failingApi = jest.fn() .mockRejectedValueOnce(new Error('Fehler 1')) .mockRejectedValueOnce(new Error('Fehler 2')) .mockResolvedValue({ data: 'Erfolg' }); const resultPromise = retryWithBackoff(failingApi, { maxRetries: 3 }); // Ersten Retry nach 1000ms auslösen await jest.advanceTimersByTimeAsync(1000); // Zweiten Retry nach 2000ms await jest.advanceTimersByTimeAsync(2000); const result = await resultPromise; expect(result.data).toBe('Erfolg'); expect(failingApi).toHaveBeenCalledTimes(3); }); });

__mocks__ Folder — Automatische Mocks

Für Module die in vielen Tests gemockt werden, erstellt Claude Code immer einen __mocks__-Ordner. Jest lädt diese Mocks automatisch.

// src/__mocks__/prisma.ts // Wird von jest.mock('@/lib/prisma') automatisch genutzt import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'; import { PrismaClient } from '@prisma/client'; const prisma = mockDeep<PrismaClient>(); beforeEach(() => { mockReset(prisma); }); export default prisma; export type { DeepMockProxy };
// In Tests: automatisch gecachter Mock jest.mock('@/lib/prisma'); import prisma from '@/lib/prisma'; import type { DeepMockProxy } from '@/lib/prisma'; const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>; it('findet Benutzer in der Datenbank', async () => { prismaMock.user.findUnique.mockResolvedValue({ id: '1', email: 'test@test.de', createdAt: new Date(), }); const user = await findUserById('1'); expect(user?.email).toBe('test@test.de'); });

jest.isolateModules — Frischer Modul-Zustand

it('Singleton verhält sich korrekt bei erstem Aufruf', () => { jest.isolateModules(() => { // Frischer Import — Singleton noch nicht initialisiert const { getConfig } = require('@/config'); process.env.NODE_ENV = 'test'; const config = getConfig(); expect(config.env).toBe('test'); }); });

4. Snapshot Tests

Snapshot

Snapshot Tests sichern serialisierbare Outputs ab. Claude Code nutzt sie für React-Komponenten, serialisierte API-Responses und komplexe Konfigurationsobjekte — aber nie als Ersatz für aussagekräftige Assertions.

toMatchSnapshot — Dateibasierte Snapshots

// src/__tests__/breadcrumb.test.tsx import React from 'react'; import { render } from '@testing-library/react'; import { Breadcrumb } from '@/components/Breadcrumb'; describe('Breadcrumb Komponente', () => { it('rendert korrekt mit mehreren Ebenen', () => { const { container } = render( <Breadcrumb items={[ { label: 'Startseite', href: '/' }, { label: 'Blog', href: '/blog' }, { label: 'Artikel', href: null }, ]} /> ); // Erster Lauf: Snapshot wird erstellt // Folgende Läufe: Vergleich mit gespeichertem Snapshot expect(container).toMatchSnapshot(); }); it('rendert korrekt mit einem Element', () => { const { container } = render( <Breadcrumb items={[{ label: 'Startseite', href: '/' }]} /> ); expect(container).toMatchSnapshot(); }); });

toMatchInlineSnapshot — Snapshots direkt im Code

it('formatiert Preise korrekt', () => { const result = formatPrice(1234.56, 'EUR'); // Claude Code nutzt Inline-Snapshots für kleine Werte // Erster Run: automatisch ausgefüllt expect(result).toMatchInlineSnapshot(`"1.234,56 €"`); }); it('serialisiert API-Response korrekt', async () => { const response = await buildApiResponse({ success: true, items: [1, 2, 3] }); expect(response).toMatchInlineSnapshot(` Object { "data": Object { "items": Array [1, 2, 3], }, "meta": Object { "count": 3, "success": true, }, } `); });

Snapshot aktualisieren & Custom Serializer

# Snapshots aktualisieren nach bewusster Änderung npx jest --updateSnapshot npx jest -u # Kurzform # Nur spezifische Test-Datei npx jest breadcrumb.test.tsx --updateSnapshot
// jest.config.ts — Custom Serializer für Datum-Objekte const config: Config = { // ... snapshotSerializers: [ 'jest-serializer-html', ], snapshotFormat: { escapeString: false, printBasicPrototype: false, }, };

Achtung: Snapshots für dynamische Inhalte (Timestamps, IDs) müssen vor dem Snapshot normalisiert werden, sonst schlagen Tests nach jedem Lauf fehl. Claude Code nutzt dafür immer expect.any(String) oder manuelle Normalisierung.

5. React Testing Library

RTL Accessibility

React Testing Library (RTL) hat sich 2026 endgültig als Standard für React-Komponenten-Tests durchgesetzt. Das Grundprinzip: Tests sollen das Nutzerverhalten widerspiegeln, nicht die Implementierung. Claude Code generiert RTL-Tests, die ausschließlich über User-zugängliche Queries arbeiten.

Installation & Setup

pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom // jest.setup.ts — EINMALIG konfigurieren import '@testing-library/jest-dom';
// jest.config.ts Ergänzung const config: Config = { testEnvironment: 'jsdom', // Für React-Tests setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'], };

render, screen & userEvent

// src/__tests__/LoginForm.test.tsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from '@/components/LoginForm'; describe('LoginForm', () => { const mockOnLogin = jest.fn(); beforeEach(() => { mockOnLogin.mockClear(); }); it('ermöglicht erfolgreiche Anmeldung', async () => { const user = userEvent.setup(); render(<LoginForm onLogin={mockOnLogin} />); // Queries über Accessibility-Attribute (bevorzugt) const emailInput = screen.getByRole('textbox', { name: /e-mail/i }); const passwordInput = screen.getByLabelText(/passwort/i); const submitBtn = screen.getByRole('button', { name: /anmelden/i }); // userEvent simuliert echtes Nutzerverhalten await user.type(emailInput, 'test@example.com'); await user.type(passwordInput, 'sicheresPasswort123!'); await user.click(submitBtn); await waitFor(() => { expect(mockOnLogin).toHaveBeenCalledWith({ email: 'test@example.com', password: 'sicheresPasswort123!', }); }); }); it('zeigt Validierungsfehler bei leerem Formular', async () => { const user = userEvent.setup(); render(<LoginForm onLogin={mockOnLogin} />); await user.click(screen.getByRole('button', { name: /anmelden/i })); expect(screen.getByText(/e-mail ist pflichtfeld/i)).toBeInTheDocument(); expect(screen.getByText(/passwort ist pflichtfeld/i)).toBeInTheDocument(); expect(mockOnLogin).not.toHaveBeenCalled(); }); });

waitFor, findBy* & async Queries

it('lädt und zeigt Benutzerdaten asynchron', async () => { jest.mock('@/api/users'); const mockFetchUser = jest.mocked(fetchUser); mockFetchUser.mockResolvedValue({ name: 'Anna Müller', role: 'admin' }); render(<UserProfile userId="u-1" />); // Warte auf asynchron geladene Inhalte const nameEl = await screen.findByText('Anna Müller'); expect(nameEl).toBeInTheDocument(); // findByRole wartet automatisch (bis 1000ms default) const badge = await screen.findByRole('status', { name: /admin/i }); expect(badge).toHaveClass('badge-admin'); }); it('zeigt Ladezustand korrekt', async () => { let resolvePromise: (val: any) => void; mockFetchUser.mockReturnValue(new Promise(resolve => { resolvePromise = resolve; })); render(<UserProfile userId="u-1" />); // Sofort: Lade-Spinner sichtbar expect(screen.getByRole('progressbar')).toBeInTheDocument(); // Promise auflösen resolvePromise!({ name: 'Test User' }); await waitFor(() => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }); });

Query-Priorität (RTL Best Practices)

Priorität Query Wann verwenden
1 (bevorzugt) getByRole Buttons, Links, Inputs, Headings
2 getByLabelText Formular-Felder mit Label
3 getByPlaceholderText Inputs ohne Label (vermeiden!)
4 getByText Statischer Text, Paragraphen
5 getByDisplayValue Selects, Inputs mit Wert
6 (letzter Ausweg) getByTestId Nur wenn keine bessere Option

6. Coverage & CI-Integration

Coverage CI/CD

Coverage-Reports ohne CI-Integration sind wertlos. Claude Code richtet immer Coverage-Schwellen und GitHub Actions Reporting ein — als Teil des initialen Test-Setups, nicht als nachträglicher Gedanke.

Coverage-Konfiguration: v8 vs Istanbul

// jest.config.ts — v8 Provider (empfohlen für Node 20+) const config: Config = { coverageProvider: 'v8', // Schneller als istanbul, keine Instrumentierung nötig collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.stories.{ts,tsx}', '!src/**/index.ts', // Re-Exports nicht zählen '!src/**/__mocks__/**', ], coverageReporters: [ 'text', // Terminal-Ausgabe 'lcov', // Für Codecov/SonarQube 'html', // Visueller Browser-Report 'json-summary', // Für GitHub Actions Summary ], coverageThreshold: { global: { branches: 80, functions: 85, lines: 85, statements: 85, }, // Pro-Pfad Schwellen für kritische Module './src/services/': { branches: 90, functions: 95, lines: 95, statements: 95, }, }, };

GitHub Actions: vollständige Jest-Pipeline

# .github/workflows/test.yml name: Tests & Coverage on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: ['20.x', '22.x'] steps: - uses: actions/checkout@v4 - name: Node.js ${{ matrix.node-version }} einrichten uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - name: pnpm installieren uses: pnpm/action-setup@v3 with: version: '9' - name: Abhängigkeiten installieren run: pnpm install --frozen-lockfile - name: TypeScript kompilieren run: pnpm tsc --noEmit - name: Jest Tests mit Coverage run: pnpm jest --coverage --ci --reporters=default --reporters=jest-github-actions-reporter - name: Coverage Summary in GitHub Summary schreiben if: always() run: | npx coverage-summary-action env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Coverage Report hochladen uses: codecov/codecov-action@v4 with: files: ./coverage/lcov.info fail_ci_if_error: true - name: Coverage Artefakt speichern uses: actions/upload-artifact@v4 if: always() with: name: coverage-report-${{ matrix.node-version }} path: coverage/ retention-days: 14

package.json Scripts

{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:ci": "jest --coverage --ci --maxWorkers=2", "test:changed": "jest --onlyChanged", "test:verbose": "jest --verbose", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand" } }

📊 Test-Suite Struktur: Claude Code Standards

Parallele Test-Ausführung optimieren

// jest.config.ts — Performance-Optimierung const config: Config = { // Worker-Zahl: CPU-Kerne - 1 (lässt Ressourcen für OS) maxWorkers: '50%', // Nur geänderte Dateien (lokal) // --onlyChanged Flag statt Konfiguration bevorzugen // Cache zwischen Runs cacheDirectory: '/tmp/jest-cache', // Lange Tests früh erkennen slowTestThreshold: 5, // Warnung ab 5 Sekunden // Bail bei erstem Fehler (CI-Modus) bail: process.env.CI === 'true' ? 1 : 0, };

Fazit: Claude Code als Test-Partner

Jest 30 mit TypeScript, React Testing Library und einer soliden CI-Pipeline ist 2026 der Standard für ernsthafte Frontend- und Backend-Projekte. Die Muster in diesem Artikel entstammen direkt aus dem Alltag mit Claude Code — einem KI-Assistenten, der nicht nur Code schreibt, sondern auch die dazugehörigen Tests.

Claude Code erkennt dabei systematisch, welche Testmuster für welche Situation passen: Fake-Timer für zeitabhängige Logik, Module-Mocks für externe Dependencies, RTL für Komponenten-Verhalten. Das Ergebnis sind Test-Suiten, die tatsächlich als Sicherheitsnetz funktionieren — nicht nur als Coverage-Metrik.

Nächste Schritte: Playwright für E2E-Tests, Storybook für Component-Isolation, Vitest als schnelle Alternative zu Jest für ESM-intensive Projekte — Claude Code unterstützt alle diese Setups und generiert vollständige Konfigurationen auf Anfrage.

Claude Code im eigenen Projekt testen

Vollständige Test-Suiten, Coverage-Reports und CI-Konfigurationen — Claude Code generiert das in Minuten statt Stunden. Jetzt 14-tägigen Trial starten.

Kostenlos testen →

Verwandte Artikel

TypeScript

TypeScript Strict Mode mit Claude Code meistern

CI/CD

GitHub Actions CI/CD mit Claude Code automatisieren

React

React Refactoring: Legacy-Code modernisieren mit Claude Code