Unit-Tests

Vitest-Konfiguration, React Testing Library-Patterns und Test-Utilities für das Testen von Komponenten, Hooks und API-Routen

Das Kit enthält 58 Unit-Test-Dateien mit 800+ Tests, die Komponenten, Hooks, API-Routen, Payment-Webhooks, Credit-System-Logik und Security-Utilities abdecken. Alle Tests laufen in einer happy-dom-Umgebung, wobei MSW jeden API-Aufruf abfängt — keine Datenbank oder externe Services erforderlich.

Konfiguration

Die Vitest-Konfiguration definiert die Testumgebung, die Setup-Datei, Umgebungsvariablen und Coverage-Einstellungen:
vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'happy-dom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    env: {
      // CRITICAL: Disable Repository mocks in unit tests
      // Unit tests mock Prisma directly and need full control over data flow
      // This flag tells Repository.isTestMode() to return false, allowing Prisma mocks to work
      DISABLE_REPOSITORY_MOCKS: 'true',

      // CRITICAL: Enable credit system in unit tests
      // Allows testing credit deduction, initialization, tier adjustment, etc.
      // E2E tests do NOT set this flag (credit system disabled, no database)
      ENABLE_CREDIT_SYSTEM_IN_TESTS: 'true',
    },
    exclude: ['node_modules/**', '.next/**', 'e2e/**'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '*.config.ts',
        '**/*.d.ts',
        '.next/',
        'src/components/ui/**', // shadcn/ui components ausschließen
      ],
    },
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
    },
  },
})
Wichtige Einstellungen:
  • happy-dom-Umgebung bietet eine leichtgewichtige DOM-Simulation (schneller als jsdom)
  • DISABLE_REPOSITORY_MOCKS — Unit-Tests mocken Prisma direkt und benötigen volle Kontrolle über die Datenschicht. Dieses Flag deaktiviert die höherstufigen Repository-Mocks, die E2E-Tests verwenden
  • ENABLE_CREDIT_SYSTEM_IN_TESTS — Aktiviert die Logik für Credit-Abzug, Initialisierung und Tier-Anpassung, damit du den vollständigen Credit-Lebenszyklus testen kannst
  • Coverage schließt node_modules/, Test-Utilities, Konfigurationsdateien und shadcn/ui-Komponenten (generierter Code) aus

Test-Setup

Die Setup-Datei läuft vor jeder Test-Suite. Sie startet den MSW-Server, erweitert Vitest-Matcher und mockt Framework-Abhängigkeiten:
src/test/setup.ts — MSW Lifecycle & Matcher
import '@testing-library/jest-dom'
import { expect, afterEach, vi, beforeAll, afterAll } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
import { server } from '@/mocks/server'

// Extend Vitest matchers
expect.extend(matchers)

// Start MSW server before all tests
beforeAll(() => {
  server.listen({
    onUnhandledRequest: 'warn',
  })
})

// Reset handlers after each test
afterEach(() => {
  cleanup()
  server.resetHandlers()
})

// Clean up after all tests
afterAll(() => {
  server.close()
})
Der MSW-Lifecycle stellt sicher, dass jeder Test mit einem sauberen Handler-Zustand beginnt. Die Einstellung onUnhandledRequest: 'warn' erkennt fehlende Handler frühzeitig während der Entwicklung.
src/test/setup.ts — Framework-Mocks
// Mock Next.js Router
vi.mock('next/navigation', () => ({
  useRouter() {
    return {
      push: vi.fn(),
      replace: vi.fn(),
      prefetch: vi.fn(),
      back: vi.fn(),
      pathname: '/',
      query: {},
      asPath: '/',
    }
  },
  usePathname() {
    return '/'
  },
  useSearchParams() {
    return new URLSearchParams()
  },
}))

// Mock Clerk v6 - Updated for async auth()
vi.mock('@clerk/nextjs', () => ({
  auth: () =>
    Promise.resolve({
      userId: 'test-user-id',
      protect: () => Promise.resolve(),
    }),
  currentUser: () =>
    Promise.resolve({
      id: 'test-user-id',
      firstName: 'Test',
      lastName: 'User',
      emailAddresses: [{ emailAddress: 'test@example.com' }],
    }),
  useAuth: () => ({
    userId: 'test-user-id',
    sessionId: 'test-session-123',
    isLoaded: true,
    isSignedIn: true,
    getToken: async () => 'test-token-123',
    signOut: async () => {},
  }),
  useUser: () => ({
    user: {
      id: 'test-user-id',
      firstName: 'Test',
      lastName: 'User',
    },
    isLoaded: true,
    isSignedIn: true,
  }),
  ClerkProvider: ({ children }: { children: React.ReactNode }) => children,
  SignIn: () => null,
  SignUp: () => null,
  UserButton: () => null,
}))

// Mock Sonner Toast
vi.mock('sonner', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    info: vi.fn(),
    warning: vi.fn(),
  },
  Toaster: () => null,
}))
Drei Framework-Mocks sind global konfiguriert:
  1. Next.js Router — Mockt useRouter, usePathname und useSearchParams, damit Komponenten, die Navigation verwenden, fehlerfrei rendern
  2. Clerk v6 — Mockt sowohl Server- (auth(), currentUser()) als auch Client-APIs (useAuth, useUser). Der auth()-Mock gibt ein Promise zurück, um Clerk v6s asynchrone Signatur zu erfüllen
  3. Sonner — Mockt Toast-Benachrichtigungen, damit du toast.success() und toast.error()-Aufrufe prüfen kannst

Tests ausführen

BefehlBeschreibung
pnpm testWatch-Modus — führt betroffene Tests bei Dateiänderungen erneut aus
pnpm test:unitEinzeldurchlauf — führt alle Tests einmal aus und beendet sich
pnpm test:uiVisuelle Oberfläche — öffnet Vitests browserbasierten Test-Explorer
pnpm test:coverageCoverage-Bericht — generiert HTML-, JSON- und Text-Berichte

Test-Organisation

Tests befinden sich in co-located __tests__/-Verzeichnissen direkt neben dem Code, den sie testen:
apps/boilerplate/src/
  lib/
    payments/
      __tests__/
        config.test.ts
        lemonsqueezy-client.test.ts
        subscriptions.test.ts
      config.ts
      lemonsqueezy-client.ts
      subscriptions.ts
  components/
    credits/
      __tests__/
        low-credit-banner.test.tsx
        usage-counter.test.tsx
      low-credit-banner.tsx
      usage-counter.tsx
Namenskonvention: <modul-name>.test.ts für Logik, <komponenten-name>.test.tsx für React-Komponenten.

Coverage nach Bereich

BereichTest-DateienWas getestet wird
Credit-System10Auto-Reset, parallele Operationen, Tier-Anpassung, Transaktionssicherheit, Trial-Helpers
Webhooks9Abrechnungszyklus, Zustandsübergänge, Subscription-Events (Erstellen, Aktualisieren, Kündigen, Pausieren, Ablaufen)
AI-Integration6Service, Provider-Factory, Rate-Limiter, Feature-Flags, RAG, Route-Guards
Security4Rate-Limiter, CORS, Sanitierung, Security-Header
Payments3Konfiguration, LemonSqueezy-Client, Subscriptions
Pricing3Konfiguration, Benutzer-Status, Variant-ID-Lookup
Komponenten2Low-Credit-Banner, Usage-Counter
Sonstiges21API-Schemas, Checkout, Navigation, Query-Prefetch, AI-Hooks, Dashboard

Testing-Patterns

Komponenten-Tests

Verwende render, screen und userEvent aus React Testing Library:
typescript
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LowCreditBanner } from '../low-credit-banner'

describe('LowCreditBanner', () => {
  it('shows warning when credits are low', () => {
    render(<LowCreditBanner credits={5} threshold={10} />)

    expect(screen.getByRole('alert')).toBeInTheDocument()
    expect(screen.getByText(/5 credits remaining/i)).toBeInTheDocument()
  })

  it('hides when dismissed', async () => {
    const user = userEvent.setup()
    render(<LowCreditBanner credits={5} threshold={10} />)

    await user.click(screen.getByRole('button', { name: /dismiss/i }))

    expect(screen.queryByRole('alert')).not.toBeInTheDocument()
  })
})

Hook-Tests

Bette Hooks in einen QueryClientProvider mit dem Test-Utility-Wrapper ein:
typescript
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useCreditDeduction } from '../use-credit-deduction'

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

describe('useCreditDeduction', () => {
  it('deducts credits on mutation', async () => {
    const { result } = renderHook(() => useCreditDeduction(), {
      wrapper: createWrapper(),
    })

    result.current.mutate({ amount: 10, reason: 'ai-chat' })

    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true)
    })
  })
})

API-Routen-Tests

Teste API-Route-Handler direkt mit fetch — MSW fängt die Anfrage ab und gibt Mock-Daten zurück:
typescript
import { server } from '@/mocks/server'
import { http, HttpResponse } from 'msw'

describe('POST /api/contact', () => {
  it('returns success for valid input', async () => {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: 'Jane Doe',
        email: 'jane@example.com',
        subject: 'Hello',
        message: 'This is a test message for the contact form.',
      }),
    })

    const data = await response.json()
    expect(data.success).toBe(true)
  })

  it('returns 400 for invalid body', async () => {
    // Standard-Handler nur für diesen Test überschreiben
    server.use(
      http.post('/api/contact', () => {
        return HttpResponse.json(
          { error: 'Validation failed' },
          { status: 400 }
        )
      })
    )

    const response = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify({}),
    })

    expect(response.status).toBe(400)
  })
})

Feature-Flag-Tests

Verwende vi.mocked() mit dynamischen Imports, um Feature-Flag-Verhalten zu testen:
typescript
import { vi } from 'vitest'

describe('AI Feature Flags', () => {
  afterEach(() => {
    vi.resetModules()
    vi.restoreAllMocks()
  })

  it('disables RAG when flag is off', async () => {
    vi.stubEnv('NEXT_PUBLIC_AI_RAG_CHAT_ENABLED', 'false')

    // Dynamischer Import, um den neuen Env-Wert zu übernehmen
    const { isRAGEnabled } = await import('@/lib/ai/feature-flags')

    expect(isRAGEnabled()).toBe(false)
  })
})

Umgebungsvariablen-Tests

Verwende das resetModules-Pattern für Code, der process.env beim Laden des Moduls liest:
typescript
describe('Config', () => {
  const originalEnv = process.env

  beforeEach(() => {
    vi.resetModules()
    process.env = { ...originalEnv }
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it('uses credit_based pricing when configured', async () => {
    process.env.NEXT_PUBLIC_PRICING_MODEL = 'credit_based'
    const { pricingModel } = await import('@/lib/config')
    expect(pricingModel).toBe('credit_based')
  })
})

Test-Utilities

Factory-Funktionen

Das Kit enthält Factory-Funktionen in apps/boilerplate/src/test/factories/, die realistische Testdaten generieren:
Factory-DateiExporte
user.factory.tscreateMockFreeUser, createMockProUser, createMockEnterpriseUser, createMockTrialUser, createMockLockedUser
subscription.factory.tscreateMockActiveSubscription, createMockTrialSubscription, createMockCancelledSubscription, createMockPausedSubscription, createMockExpiredSubscription
file.factory.tscreateMockFile, createMockPdfFile, createMockImageFile, createMockLargeFile, createMockFileList
ai-conversation.factory.tscreateMockConversation, createMockMessage
ai-usage.factory.tscreateMockUsageRecord (FAQ, Chat, Completion, Streaming, Embeddings)
typescript
import {
  createMockProUser,
  createMockActiveSubscription,
} from '@/test/factories'

const user = createMockProUser()
const subscription = createMockActiveSubscription()

TanStack Query Wrapper

Viele Hooks benötigen einen QueryClientProvider. Erstelle einen wiederverwendbaren Wrapper:
typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

export function createQueryWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false, gcTime: 0 },
      mutations: { retry: false },
    },
  })

  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
Das Setzen von retry: false und gcTime: 0 verhindert, dass gecachte Daten und Retry-Verzögerungen Tests instabil machen.

Deinen ersten Test schreiben

1

Testdatei erstellen

Erstelle ein __tests__/-Verzeichnis neben der Komponente und füge eine Testdatei hinzu:
apps/boilerplate/src/components/dashboard/
  __tests__/
    stats-card.test.tsx    <-- neue Datei
  stats-card.tsx
2

Test schreiben

typescript
import { render, screen } from '@testing-library/react'
import { StatsCard } from '../stats-card'

describe('StatsCard', () => {
  it('renders the title and value', () => {
    render(<StatsCard title="Total Users" value={1234} />)

    expect(screen.getByText('Total Users')).toBeInTheDocument()
    expect(screen.getByText('1,234')).toBeInTheDocument()
  })

  it('shows trend indicator when provided', () => {
    render(<StatsCard title="Revenue" value={5000} trend="+12%" />)

    expect(screen.getByText('+12%')).toBeInTheDocument()
  })
})
3

Test ausführen

bash
cd apps/boilerplate && pnpm test src/components/dashboard/__tests__/stats-card.test.tsx
Oder verwende den Watch-Modus für kontinuierliches Feedback:
bash
pnpm test -- --watch

Coverage

Die Coverage wird mit dem v8-Provider generiert (V8s integrierte Code-Coverage, schneller als Istanbul):
bash
pnpm test:coverage
Berichte werden in drei Formaten generiert:
FormatAusgabeZweck
textTerminalSchnelle Übersicht während der Entwicklung
jsoncoverage/coverage-final.jsonCI-Integration und Tooling
htmlcoverage/index.htmlDetaillierter interaktiver Bericht pro Datei

Von Coverage ausgeschlossen

Die folgenden Pfade sind ausgeschlossen, da sie generierten oder Drittanbieter-Code enthalten:
  • node_modules/ — Abhängigkeiten
  • apps/boilerplate/src/test/ — Test-Utilities selbst
  • *.config.ts — Build-Konfiguration
  • **/*.d.ts — Typdeklarationen
  • .next/ — Build-Output
  • apps/boilerplate/src/components/ui/** — Generierte shadcn/ui-Komponenten

Wichtige Dateien

DateiZweck
apps/boilerplate/vitest.config.tsTestumgebung, Setup-Datei, Coverage, Aliases
apps/boilerplate/src/test/setup.tsMSW-Lifecycle, jest-dom-Matcher, Framework-Mocks
apps/boilerplate/src/test/factories/Factory-Funktionen zur Generierung von Testdaten
apps/boilerplate/src/mocks/server.tsNode.js-MSW-Server, der von allen Unit-Tests genutzt wird
apps/boilerplate/src/mocks/handlers.tsStandard-Handler für alle API-Endpunkte