Das Kit verwendet Mock Service Worker (MSW), um HTTP-Anfragen auf Netzwerkebene abzufangen und Mock-Responses zurückzugeben. Das bedeutet, dass deine Tests und die lokale Entwicklungsumgebung keine Datenbank, keine externen APIs und keine Service-Accounts benötigen — MSW ersetzt alles durch deterministische, sofortige Responses.
Die Mock-Schicht umfasst 12 Handler-Dateien, die jede API-Oberfläche abdecken: AI-Chat mit Streaming (inkl. Vision/Bildanalyse), Bildgenerierung, Payment-Subscriptions, Credit-System, Rate-Limiting, Datei-Uploads, E-Mail und mehr.
Architektur
MSW läuft in zwei Modi, abhängig von der Umgebung:
| Modus | Modul | Verwendet von | Abfangmethode |
|---|---|---|---|
| Server | apps/boilerplate/src/mocks/server.ts | Vitest (Node.js) | Patcht http/https-Module |
| Browser | apps/boilerplate/src/mocks/browser.ts | E2E-Tests, Dev-Server | Service Worker (mockServiceWorker.js) |
Beide Modi teilen sich dieselben Handler-Dateien — du schreibst Handler einmal und sie funktionieren überall.
Server-Modus (Vitest)
src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
// This configures a request mocking server with the given request handlers.
// This server will be used in tests to intercept and mock HTTP requests.
export const server = setupServer(...handlers)
// Establish API mocking before all tests.
// This ensures that the mock server is running and ready to intercept requests.
export function startServer() {
server.listen({
onUnhandledRequest: 'warn', // Warn about unhandled requests in tests
})
}
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
export function resetServer() {
server.resetHandlers()
}
// Clean up after the tests are finished.
export function stopServer() {
server.close()
}
Der Server wird in
apps/boilerplate/src/test/setup.ts vor allen Tests gestartet und danach geschlossen. Jeder Test setzt Handler zurück, um Zustandsübertragungen zu verhindern.Browser-Modus (E2E / Entwicklung)
src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
// This configures a Service Worker with the given request handlers.
// This worker will be used in the browser during development.
export const worker = setupWorker(...handlers)
// Start the worker
export async function startWorker() {
if (typeof window === 'undefined') {
return
}
// Only start the worker in development, test, or demo mode
// E2E tests need MSW to intercept API calls and return mocked data
// Demo mode on Vercel runs with NODE_ENV=production but still needs MSW
if (
process.env.NODE_ENV !== 'development' &&
process.env.NODE_ENV !== 'test' &&
process.env.NEXT_PUBLIC_DEMO_MODE !== 'true'
) {
return
}
// Check if MSW is enabled via environment variable
if (process.env.NEXT_PUBLIC_MSW_ENABLED !== 'true') {
console.info('MSW is disabled. Set NEXT_PUBLIC_MSW_ENABLED=true to enable.')
return
}
try {
await worker.start({
onUnhandledRequest: 'bypass', // Let unhandled requests pass through
serviceWorker: {
url: '/mockServiceWorker.js',
},
})
console.info('🔶 MSW (Mock Service Worker) started successfully')
} catch (error) {
console.error('Failed to start MSW:', error)
}
}
// Stop the worker
export function stopWorker() {
if (typeof window !== 'undefined' && worker) {
worker.stop()
}
}
Der Browser-Modus wird aktiviert, wenn:
- Die Umgebung
developmentodertestist NEXT_PUBLIC_MSW_ENABLEDauftruegesetzt ist
In der Entwicklung ermöglicht MSW das Bauen von Features gegen realistische API-Responses, ohne Backend-Services laufen zu lassen. In E2E-Tests liefert er deterministische Daten für Playwright-Assertions.
MSW Utility-Exporte
src/mocks/index.ts
// Re-export all mock utilities for convenient imports
export { handlers } from './handlers'
export { server, startServer, resetServer, stopServer } from './server'
export { worker, startWorker, stopWorker } from './browser'
// Helper to check if MSW is enabled
export const isMSWEnabled = () => {
if (typeof window === 'undefined') {
// Server-side: MSW is always enabled in tests
return process.env.NODE_ENV === 'test'
}
// Client-side: Check environment variable
return process.env.NEXT_PUBLIC_MSW_ENABLED === 'true'
}
Der
isMSWEnabled()-Helper erkennt die aktuelle Umgebung: Serverseitig prüft er NODE_ENV, clientseitig das Flag NEXT_PUBLIC_MSW_ENABLED.Handler-Inventar
Alle Handler werden in
apps/boilerplate/src/mocks/handlers.ts zusammengeführt und in ein einziges Array gestreut:src/mocks/handlers.ts — Handler-Imports
import { http, HttpResponse } from 'msw'
import { isValidRequestBody } from '@/lib/email/types'
import {
lemonsqueezyHandlers,
getTierFromSubscription,
} from './lemonsqueezy-handlers'
import { aiHandlers } from './ai-handlers'
import { dashboardHandlers } from './dashboard-handlers'
import { settingsHandlers } from './settings-handlers'
import { billingHandlers } from './billing-handlers'
import { checkoutHandlers } from './checkout-handlers'
import { demoHandlers } from './demo-handlers'
import { creditHistoryHandlers } from './credit-history-handlers'
import { creditPreferencesHandlers } from './credit-preferences-handlers'
import { imageGenHandlers } from './image-gen-handlers'
src/mocks/handlers.ts — Handler-Aggregation
export const handlers = [
// Include all Lemon Squeezy handlers
...lemonsqueezyHandlers,
// Include all AI handlers
...aiHandlers,
// Include all Dashboard handlers (NEW)
...dashboardHandlers,
// Include all Settings handlers (NEW)
...settingsHandlers,
// Include all Billing handlers (NEW)
...billingHandlers,
// Include all Checkout handlers (PRP: Direct Checkout)
...checkoutHandlers,
// Include all Demo handlers (Demo Mode: tier-aware mocks)
...demoHandlers,
// Include all Credit History handlers (NESAI-037)
...creditHistoryHandlers,
// Include all Credit Preferences handlers (NESAI-054)
...creditPreferencesHandlers,
// Include all Image Generation handlers (NESAI-062)
...imageGenHandlers,
Handler-Dateien
| Datei | Gemockte APIs | Besondere Features |
|---|---|---|
handlers.ts | Benutzer, Posts, Auth, Dashboard, E-Mail, Dateien, Health, Fehler | Paginierung, Datei-Upload-Validierung, verzögerte Responses |
ai-handlers.ts | AI-Chat, RAG-Bot, Konversationen, Usage-Tracking | SSE-Streaming (OpenAI-kompatibel), vision-aware Credit-Abzug (Text: 1,5, Bild: 2,0), Token-Schätzung |
lemonsqueezy-handlers.ts | Subscriptions, Produkte, Varianten, Webhooks | Dynamische Tier-Berechnung, Kulanzzeiträume, Trial-Behandlung |
dashboard-handlers.ts | Dashboard-Statistiken, Analytics, Activity-Feed | Realistische Diagrammdaten, Zeitreihen-Responses |
settings-handlers.ts | Benutzereinstellungen, Präferenzen | Profilaktualisierungen, Präferenz-Umschalter |
billing-handlers.ts | Abrechnungsverlauf, Rechnungen, Zahlungsmethoden | Subscription-Zustandsverwaltung |
checkout-handlers.ts | Direkter Checkout, Checkout-Sessions | Checkout-URL-Generierung, Varianten-Validierung |
credit-history-handlers.ts | Credit-Transaktionsverlauf | Paginierter Credit-Verlauf, Transaktionstypen |
credit-preferences-handlers.ts | Credit-Präferenzen | Bonus-Credit-Auto-Use-Präferenz-Umschalter |
image-gen-handlers.ts | Bildgenerierung | GPT Image API-Mock mit Base64-Responses |
rate-limit-handlers.ts | Rate-Limiting | Rate-Limit-Prüfungs- und Reset-Mocks |
demo-handlers.ts | Alle Demo-Modus-Endpunkte | Tier-aware Responses, Credit-Balance-Integration |
Handler-Patterns
Einfache GET/POST-Handler
typescript
import { http, HttpResponse } from 'msw'
export const myHandlers = [
// GET — statische Daten zurückgeben
http.get('/api/posts', () => {
return HttpResponse.json({
posts: [{ id: '1', title: 'Hello World' }],
total: 1,
})
}),
// POST — Request-Body prüfen und antworten
http.post('/api/posts', async ({ request }) => {
const body = await request.json()
return HttpResponse.json(
{ id: 'new-post', ...body },
{ status: 201 }
)
}),
]
Request-Inspektion
Auf Header, Query-Parameter und Routen-Parameter zugreifen:
typescript
http.get('/api/posts/:slug', ({ params, request }) => {
const { slug } = params
const url = new URL(request.url)
const page = url.searchParams.get('page') || '1'
const authHeader = request.headers.get('authorization')
// Extrahierte Werte nutzen, um Response anzupassen
return HttpResponse.json({ slug, page })
})
Fehler-Simulation
Bestimmte Status-Codes zurückgeben, um Fehlerbehandlung zu testen:
typescript
// Permanenter Fehler-Endpunkt für Tests
http.get('/api/error/500', () => {
return new HttpResponse(null, {
status: 500,
statusText: 'Internal Server Error',
})
})
// Bedingter Fehler basierend auf Request-Inhalt
http.post('/api/contact', async ({ request }) => {
const body = await request.json()
if (!body?.email) {
return HttpResponse.json(
{ error: 'Email is required' },
{ status: 400 }
)
}
return HttpResponse.json({ success: true })
})
Streaming-Responses (SSE)
Die AI-Handler demonstrieren Server-Sent Events für Streaming-Chat-Responses:
typescript
http.post('/api/ai/chat', () => {
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
// Chunks im OpenAI-kompatiblen Format senden
const chunks = ['Hello', ' from', ' AI', '!']
chunks.forEach((chunk, i) => {
const data = JSON.stringify({
choices: [{ delta: { content: chunk } }],
})
controller.enqueue(
encoder.encode(`data: ${data}\n\n`)
)
})
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
},
})
return new HttpResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
})
Paginierung
Die Datei
handlers.ts demonstriert Paginierung mit Query-Parameter-Extraktion:typescript
http.get('/api/posts', ({ request }) => {
const url = new URL(request.url)
const page = parseInt(url.searchParams.get('page') || '1')
const limit = parseInt(url.searchParams.get('limit') || '10')
const startIndex = (page - 1) * limit
const paginatedPosts = allPosts.slice(startIndex, startIndex + limit)
return HttpResponse.json({
posts: paginatedPosts,
total: allPosts.length,
page,
totalPages: Math.ceil(allPosts.length / limit),
})
})
Tier-abhängige Responses
Die Payment-Handler berechnen Subscription-Tiers dynamisch basierend auf dem Status und behandeln Randfälle wie Kulanzzeiträume und abgelaufene Trials:
typescript
http.get('/api/payments/subscription', () => {
const subscription = {
status: 'active',
variantId: 'var_pro_monthly',
currentPeriodEnd: new Date(
Date.now() + 30 * 24 * 60 * 60 * 1000
).toISOString(),
}
// Dynamische Tier-Berechnung — behandelt active, trial,
// cancelled, paused, expired, past_due Zustände
const tier = getTierFromSubscription(subscription)
return HttpResponse.json({
subscription,
tier,
hasActiveSubscription: subscription.status === 'active',
})
})
Pro-Test-Handler-Überschreibungen
In Vitest (server.use)
Überschreibe jeden Handler für einen einzelnen Test mit
server.use(). Die Überschreibung wird automatisch durch server.resetHandlers() in afterEach bereinigt:typescript
import { server } from '@/mocks/server'
import { http, HttpResponse } from 'msw'
describe('Dashboard', () => {
it('shows empty state when no data', async () => {
// Dashboard-Handler nur für diesen Test überschreiben
server.use(
http.get('/api/dashboard', () => {
return HttpResponse.json({
stats: { totalPosts: 0, totalViews: 0 },
recentActivity: [],
})
})
)
// Test erhält jetzt leere Daten vom überschriebenen Handler
render(<Dashboard />)
await expect(screen.getByText(/no data yet/i)).toBeInTheDocument()
})
it('shows error state on API failure', async () => {
server.use(
http.get('/api/dashboard', () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
)
})
)
render(<Dashboard />)
await expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
})
})
Verwende
server.use(), um Handler pro Test zu überschreiben, anstatt die gemeinsamen Handler-Dateien zu modifizieren. Das hält die Standard-Happy-Path-Handler stabil und macht jeden Test in sich geschlossen.Rufe immer
server.resetHandlers() in afterEach auf (dies ist bereits in apps/boilerplate/src/test/setup.ts konfiguriert). Ohne dies übertragen sich Überschreibungen aus einem Test auf nachfolgende Tests und verursachen flaky Fehler, die schwer zu diagnostizieren sind.In Playwright (page.route)
Für E2E-Tests verwende Playwrights
page.route(), um Anfragen auf Browser-Ebene abzufangen:typescript
import { test, expect } from '@playwright/test'
test('shows error when subscription API fails', async ({ page }) => {
await page.route('/api/payments/subscription', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Service unavailable' }),
})
})
await page.goto('/dashboard/billing')
await expect(page.getByText(/unable to load subscription/i)).toBeVisible()
})
Neue Handler hinzufügen
1
Handler-Datei erstellen
Erstelle eine neue Datei in
apps/boilerplate/src/mocks/ gemäß der Namenskonvention <feature>-handlers.ts:typescript
// src/mocks/search-handlers.ts
import { http, HttpResponse } from 'msw'
export const searchHandlers = [
http.get('/api/search', ({ request }) => {
const url = new URL(request.url)
const query = url.searchParams.get('q') || ''
return HttpResponse.json({
results: [
{ id: '1', title: `Result for "${query}"`, score: 0.95 },
],
total: 1,
})
}),
]
2
In handlers.ts registrieren
Die neuen Handler importieren und in das Haupt-Handler-Array streuen:
typescript
// src/mocks/handlers.ts
import { searchHandlers } from './search-handlers'
export const handlers = [
...searchHandlers,
// ... bestehende Handler
]
3
Mit Pro-Test-Überschreibungen testen
Tests schreiben, die den Standard-Handler verwenden und Randfälle überschreiben:
typescript
import { server } from '@/mocks/server'
import { http, HttpResponse } from 'msw'
describe('Search', () => {
it('returns results from default handler', async () => {
const res = await fetch('/api/search?q=test')
const data = await res.json()
expect(data.results).toHaveLength(1)
})
it('handles empty results', async () => {
server.use(
http.get('/api/search', () => {
return HttpResponse.json({ results: [], total: 0 })
})
)
const res = await fetch('/api/search?q=nothing')
const data = await res.json()
expect(data.results).toHaveLength(0)
})
})
MSW-Handler werden von
MSWProvider (eine Client-Komponente) importiert, was bedeutet, dass sie für den Browser gebündelt werden. Das Importieren von Node.js-spezifischen Modulen führt zu Produktions-Build-Fehlern.Verbotene Imports in Handler-Dateien:
@prisma/client— benötigtfs,path,crypto@/test/factories— kann Prisma-Typen importierenfs,path,crypto— Node.js-Built-in-Module- Jede server-only-Bibliothek
Build-Fehlersignatur: Wenn du
Module not found: Can't resolve 'fs' während pnpm build siehst, verfolge die Import-Kette von apps/boilerplate/src/mocks/handlers.ts — ein Handler importiert ein server-only-Modul.Lösung: Definiere alle Mock-Typen und Factory-Funktionen inline innerhalb der Handler-Datei. Verwende
import type { ... } für reine Typ-Imports (diese werden zur Kompilierzeit entfernt). Für datenbankspezifische Testlogik verwende stattdessen server.use()-Pro-Test-Überschreibungen.Demo-Modus-Integration
Handler-Dateien integrieren sich mit dem
demoCreditStore, um realistischen Credit-Abzug im Demo-Modus bereitzustellen. Die AI-Handler prüfen das Credit-Guthaben vor der Verarbeitung von Anfragen:typescript
// In ai-handlers.ts (vereinfacht)
http.post('/api/ai/chat', async ({ request }) => {
const { credits } = demoCreditStore.getState()
if (credits <= 0) {
return HttpResponse.json(
{ error: 'No credits remaining' },
{ status: 402 }
)
}
// Credits basierend auf geschätztem Token-Verbrauch abziehen
demoCreditStore.getState().deductCredits(10)
// Streaming-Response zurückgeben...
})
Dies ermöglicht es dem Demo-Modus der Marketing-Site, realistisches Credit-Abzugs-Verhalten anzuzeigen, ohne Backend-Services zu benötigen.
Umgebungskonfiguration
| Variable | Standard | Zweck |
|---|---|---|
NEXT_PUBLIC_MSW_ENABLED | false | MSW im Browser aktivieren (in apps/boilerplate/.env.test auf true gesetzt) |
DISABLE_REPOSITORY_MOCKS | false | Repository-Schicht in Unit-Tests deaktivieren (in apps/boilerplate/vitest.config.ts auf true gesetzt) |
NODE_ENV | — | MSW-Server-Modus aktiviert sich, wenn NODE_ENV === 'test' |
MSW-Lifecycle in Tests
Der MSW-Server-Lifecycle wird in
apps/boilerplate/src/test/setup.ts verwaltet:typescript
// Vor allen Tests — Server starten
beforeAll(() => {
server.listen({ onUnhandledRequest: 'warn' })
})
// Nach jedem Test — Überschreibungen zurücksetzen
afterEach(() => {
cleanup()
server.resetHandlers()
})
// Nach allen Tests — Server stoppen
afterAll(() => {
server.close()
})
Fehlerbehebung
Warnungen bei nicht behandelten Anfragen
Wenn du
[MSW] Warning: intercepted a request without a matching request handler siehst, bedeutet das, dass ein Test einen API-Aufruf macht, den kein Handler abdeckt. Lösungen:- Einen Handler hinzufügen in der entsprechenden Handler-Datei
- Pro-Test überschreiben mit
server.use(), wenn der Endpunkt testspezifisch ist - URL prüfen — Tippfehler in Endpunkt-Pfaden sind die häufigste Ursache
Handler-Reihenfolge
MSW gleicht Handler in Array-Reihenfolge ab (erster Treffer gewinnt). Wenn du einen generischen Handler wie
http.get('/api/*') hast, platziere ihn nach spezifischeren Handlern:typescript
export const handlers = [
http.get('/api/user/:id', specificHandler), // Zuerst geprüft
http.get('/api/user', listHandler), // Danach geprüft
// Catch-all-Handler nicht vor spezifische setzen
]
Veraltete Überschreibungen zwischen Tests
Wenn Tests mit unerwarteten Daten fehlschlagen, prüfe, ob
server.resetHandlers() in afterEach aufgerufen wird. Ohne dies bleiben server.use()-Überschreibungen über Tests hinweg bestehen. Die Setup-Datei konfiguriert dies automatisch, aber benutzerdefinierte Test-Setups könnten es übersehen.MSW fängt im Browser nicht ab
Wenn der Entwicklungsserver keine Anfragen abfängt:
- Prüfe, ob
NEXT_PUBLIC_MSW_ENABLED=trueinapps/boilerplate/.env.localgesetzt ist - Prüfe, ob
apps/boilerplate/public/mockServiceWorker.jsexistiert (führecd apps/boilerplate && npx msw init public/aus, um es neu zu generieren) - Suche nach der Konsolenmeldung
MSW (Mock Service Worker) started successfully
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/src/mocks/handlers.ts | Haupt-Handler-Aggregator mit grundlegenden API-Handlern |
apps/boilerplate/src/mocks/server.ts | Node.js-MSW-Server für Vitest |
apps/boilerplate/src/mocks/browser.ts | Browser-MSW-Worker für E2E und Entwicklung |
apps/boilerplate/src/mocks/index.ts | Barrel-Exporte und isMSWEnabled()-Helper |
apps/boilerplate/src/mocks/ai-handlers.ts | AI-Chat, RAG, Streaming, Credit-Abzug |
apps/boilerplate/src/mocks/lemonsqueezy-handlers.ts | Payment-Subscriptions, Produkte, Tier-Berechnung |
apps/boilerplate/src/mocks/demo-handlers.ts | Demo-Modus tier-aware Responses |
apps/boilerplate/src/test/setup.ts | MSW-Lifecycle (Start, Reset, Schließen) |