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 verwendenENABLE_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
Sowohl Unit-Tests als auch E2E-Tests laufen mit
NODE_ENV=test, benötigen aber unterschiedliches Credit-System-Verhalten: Unit-Tests sollen den vollständigen Credit-Lebenszyklus durchlaufen (Abzug, Reset, Tier-Anpassung), während E2E-Tests Credit-Operationen überspringen sollen (keine Datenbank-Schreibvorgänge nötig, nur UI-Testing). Das Flag ENABLE_CREDIT_SYSTEM_IN_TESTS ist in vitest.config.ts gesetzt, aber NICHT in playwright.config.ts, sodass jeder Test-Typ das korrekte Verhalten erhält. Wende dieses Muster (ENABLE_{FEATURE}_IN_TESTS) an, wenn ein Feature unterschiedliches Verhalten zwischen Unit- und E2E-Tests benötigt.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:
- Next.js Router — Mockt
useRouter,usePathnameunduseSearchParams, damit Komponenten, die Navigation verwenden, fehlerfrei rendern - Clerk v6 — Mockt sowohl Server- (
auth(),currentUser()) als auch Client-APIs (useAuth,useUser). Derauth()-Mock gibt ein Promise zurück, um Clerk v6s asynchrone Signatur zu erfüllen - Sonner — Mockt Toast-Benachrichtigungen, damit du
toast.success()undtoast.error()-Aufrufe prüfen kannst
Tests ausführen
| Befehl | Beschreibung |
|---|---|
pnpm test | Watch-Modus — führt betroffene Tests bei Dateiänderungen erneut aus |
pnpm test:unit | Einzeldurchlauf — führt alle Tests einmal aus und beendet sich |
pnpm test:ui | Visuelle Oberfläche — öffnet Vitests browserbasierten Test-Explorer |
pnpm test:coverage | Coverage-Bericht — generiert HTML-, JSON- und Text-Berichte |
pnpm test:ui öffnet eine interaktive Browser-Oberfläche, mit der du Tests filtern, Komponenten-Output ansehen und Konsolen-Logs prüfen kannst. Es ist die schnellste Methode, um einen fehlschlagenden Test zu debuggen.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
| Bereich | Test-Dateien | Was getestet wird |
|---|---|---|
| Credit-System | 10 | Auto-Reset, parallele Operationen, Tier-Anpassung, Transaktionssicherheit, Trial-Helpers |
| Webhooks | 9 | Abrechnungszyklus, Zustandsübergänge, Subscription-Events (Erstellen, Aktualisieren, Kündigen, Pausieren, Ablaufen) |
| AI-Integration | 6 | Service, Provider-Factory, Rate-Limiter, Feature-Flags, RAG, Route-Guards |
| Security | 4 | Rate-Limiter, CORS, Sanitierung, Security-Header |
| Payments | 3 | Konfiguration, LemonSqueezy-Client, Subscriptions |
| Pricing | 3 | Konfiguration, Benutzer-Status, Variant-ID-Lookup |
| Komponenten | 2 | Low-Credit-Banner, Usage-Counter |
| Sonstiges | 21 | API-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)
})
})
Das
server.use()-Pattern ermöglicht es dir, jeden MSW-Handler für einen einzelnen Test zu überschreiben, ohne gemeinsame Handler zu modifizieren. Die Überschreibung wird automatisch durch server.resetHandlers() in afterEach bereinigt. Weitere Patterns findest du unter API Mocking.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)
})
})
Verwende beim Testen von Umgebungsvariablen oder Feature-Flags
vi.resetModules() in afterEach, um sicherzustellen, dass jeder Test eine frische Modulinstanz erhält. Ohne dies kann gecachter Modulzustand aus einem vorherigen Test zu falsch-positiven Ergebnissen führen.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-Datei | Exporte |
|---|---|
user.factory.ts | createMockFreeUser, createMockProUser, createMockEnterpriseUser, createMockTrialUser, createMockLockedUser |
subscription.factory.ts | createMockActiveSubscription, createMockTrialSubscription, createMockCancelledSubscription, createMockPausedSubscription, createMockExpiredSubscription |
file.factory.ts | createMockFile, createMockPdfFile, createMockImageFile, createMockLargeFile, createMockFileList |
ai-conversation.factory.ts | createMockConversation, createMockMessage |
ai-usage.factory.ts | createMockUsageRecord (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:
| Format | Ausgabe | Zweck |
|---|---|---|
text | Terminal | Schnelle Übersicht während der Entwicklung |
json | coverage/coverage-final.json | CI-Integration und Tooling |
html | coverage/index.html | Detaillierter interaktiver Bericht pro Datei |
Von Coverage ausgeschlossen
Die folgenden Pfade sind ausgeschlossen, da sie generierten oder Drittanbieter-Code enthalten:
node_modules/— Abhängigkeitenapps/boilerplate/src/test/— Test-Utilities selbst*.config.ts— Build-Konfiguration**/*.d.ts— Typdeklarationen.next/— Build-Outputapps/boilerplate/src/components/ui/**— Generierte shadcn/ui-Komponenten
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/vitest.config.ts | Testumgebung, Setup-Datei, Coverage, Aliases |
apps/boilerplate/src/test/setup.ts | MSW-Lifecycle, jest-dom-Matcher, Framework-Mocks |
apps/boilerplate/src/test/factories/ | Factory-Funktionen zur Generierung von Testdaten |
apps/boilerplate/src/mocks/server.ts | Node.js-MSW-Server, der von allen Unit-Tests genutzt wird |
apps/boilerplate/src/mocks/handlers.ts | Standard-Handler für alle API-Endpunkte |