Why Scenarist?
What is Scenario-Based Testing?
Section titled “What is Scenario-Based Testing?”Scenario-based testing is an integration testing approach where your real application code executes while external dependencies (third-party APIs, microservices) return controlled responses. Unlike true end-to-end tests that use zero mocks, scenario-based tests mock only the external services you don’t control.
| Testing Approach | Your Code | External APIs | Best For |
|---|---|---|---|
| Unit Tests | Mocked | Mocked | Isolated function logic |
| Scenario-Based Tests | Real | Mocked | Application behavior with controlled dependencies |
| End-to-End Tests | Real | Real | Full system validation (production-like) |
Why “scenario-based”? Because you define complete backend scenarios (success, error, timeout, user tiers) and switch between them at runtime. Each test selects a scenario that describes the complete external API state, enabling comprehensive testing without external dependencies.
The key distinction from E2E: True end-to-end tests use real external APIs with zero mocks—ideal for validating complete production behavior, but slow, expensive, and limited in edge case coverage. Scenario-based tests give you the speed of unit tests with the realism of integration tests by running your real code against controlled external responses.
The Testing Gap
Section titled “The Testing Gap”Modern web development has blurred the traditional separation between frontend and backend code. Frameworks like Next.js, Remix, SvelteKit, and SolidStart run server-side logic alongside UI components. Traditional backends built with Express, Hono, or Fastify face the same challenge: all make HTTP calls to external services (Stripe, Auth0, SendGrid) that need different behaviors in tests.
This creates a testing challenge:
Server Components, loaders, and API routes execute server-side but are defined alongside components. Your UI code calls external APIs directly on the server. Testing this requires either mocking framework internals or running full end-to-end tests.
Traditional backend services call the same external APIs. Testing payment flows, authentication errors, or email delivery requires simulating different API responses.
What Scenarist Offers
- Simple Architecture — Just an HTTP header (
x-scenarist-test-id). No Docker, no separate processes, no complex network configuration - Test ID Isolation — Run hundreds of parallel tests with different scenarios against one server. Each test's header routes to its own scenario
- Runtime Switching — Change scenarios mid-test without restarts (retry flows, error recovery)
- First-Class Playwright — Dedicated fixtures with type-safe scenarios and automatic test ID handling
- Response Sequences — Built-in polling, retry flows, state machines
- Stateful Mocks — Capture request values, inject into responses. State is isolated per test ID, so parallel tests never conflict
- Advanced Matching — Body, headers, query params, regex with specificity-based selection
- Framework Adapters — Not thin wrappers—they solve real problems. For example, the Next.js adapter includes built-in singleton protection for the module duplication issue that breaks MSW
- Developer Tools (Roadmap) — Planned browser-based plugin for switching scenarios during development and debugging—making scenario exploration instant and visual
Testing Options and Their Trade-offs
Section titled “Testing Options and Their Trade-offs”Unit tests can test server-side logic, but require mocking framework internals (Next.js fetch, cookies, headers) or HTTP clients. This creates distance between test execution and production behavior.
End-to-end tests provide confidence by testing the complete system, but cannot reach most edge case states. How do you make Stripe return a specific decline code? Or Auth0 timeout? Or SendGrid fail with a particular error? You can’t control real external APIs to test these scenarios. Testing the few scenarios you can reach would also be prohibitively slow.
Between these approaches lies a gap: Testing server-side HTTP behavior with different external API responses, without browser overhead or extensive framework mocking.
Scenarist fills this gap by testing your server-side HTTP layer with mocked external APIs. Your code—Server Components, loaders, middleware, business logic—executes normally. Only HTTP requests (fetch, axios, etc.) are intercepted, returning scenario-defined responses based on test ID. This enables testing full user journeys through the browser using Playwright helpers, with each test isolated and running in parallel.
Test extensive external API scenarios in parallel without expensive cloud API calls or complex test infrastructure.
Next.js Multi-Process Handling (Solved)
Next.js presents a unique challenge for MSW-based testing. It has a well-documented singleton problem where webpack bundles the same module multiple times, breaking classic singleton patterns. This is compounded by MSW's challenges with Next.js's process model—Next.js keeps multiple Node.js processes that make global module patches difficult to maintain.
Scenarist solves this automatically. The Next.js adapter includes built-in globalThis singleton guards that ensure only one MSW instance exists, regardless of how Next.js loads your modules. You don't need to understand Next.js internals or implement manual workarounds—just use export const scenarist = createScenarist(...) and Scenarist handles the complexity.
What You Can Test
Section titled “What You Can Test”When your app calls external HTTP APIs, Scenarist gives you full control. You can test complete user journeys—from browser interaction through Server Components, API routes, and middleware—with your real server-side code executing, while you control exactly what responses come back from external services.
Perfect For
Section titled “Perfect For”- Server Components fetching from external APIs (Stripe, Auth0, SendGrid)
- API routes that call third-party services
- Middleware that validates tokens or checks permissions
- Full user journeys through real frontend + backend code
// Server Component - Your real code executesexport default async function CheckoutPage() { // ✅ This call is intercepted - you control the response const payment = await fetch('https://api.stripe.com/v1/charges', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.STRIPE_KEY}` }, body: JSON.stringify({ amount: 5000 }), });
// Your real rendering logic const result = await payment.json(); return <PaymentConfirmation status={result.status} />;}Test any scenario: Payment success, card declined, network timeout, rate limiting, webhook failures—all controlled by your test scenarios, running in parallel.
The Requirement: Network Requests
Section titled “The Requirement: Network Requests”Scenarist intercepts HTTP requests that traverse the network. This works because MSW (Mock Service Worker) operates at the network level, intercepting requests from any HTTP client (fetch, axios, etc.).
What this means in practice:
// ✅ WORKS - External APIconst stripe = await fetch("https://api.stripe.com/v1/products");
// ✅ WORKS - Different port on localhostconst products = await fetch("http://localhost:3001/products");
// ❌ Does NOT work - Same host/port internal routeconst products = await fetch("http://localhost:3000/api/products");Why internal routes don’t work: When a Server Component calls an API route on the same host/port, Next.js handles this internally without making a network request. MSW only sees requests that go through the network stack.
What Cannot Be Intercepted
Section titled “What Cannot Be Intercepted”- Direct database access (PostgreSQL, MongoDB, Prisma) - no HTTP request
- Internal API routes on the same host/port - Next.js internal routing
- File system operations - no HTTP request
- WebSocket connections - MSW supports WebSockets, but Scenarist’s scenario system focuses on HTTP
If your app uses direct database access: See Testing Database Apps for strategies. We recommend the Repository Pattern for scalable parallel testing with the same test ID isolation model as Scenarist.
Why Framework Documentation Recommends E2E
Section titled “Why Framework Documentation Recommends E2E”This gap is evident in how framework authors struggle to provide testing guidance. The Next.js testing docs focus primarily on unit testing and E2E testing, acknowledging that async Server Components present unique testing challenges. Remix testing guidance notes the complexity of testing components that depend on Remix context and loaders. SvelteKit faces similar challenges with server route testing.
The pattern is clear: when “frontend” components run on the server and call external APIs directly, traditional testing approaches break down. Scenarist fills this gap by testing real server-side code with mocked external APIs.
Testing Behavior, Not Implementation
Section titled “Testing Behavior, Not Implementation”Scenarist enables behavior-focused testing by letting you test your server’s response to different external API behaviors without mocking internal implementation details.
Your tests describe scenarios:
- “Premium user checkout with valid payment”
- “Payment declined due to insufficient funds”
- “Auth0 timeout during login”
Not implementation details:
“Mock stripe.charges.create to throw error”“Stub authClient.getSession to return null”“Mock sendgrid.send to resolve with 500”
This follows Test-Driven Development principles: tests document expected behavior, implementation details can change as long as behavior stays consistent.
Learn about our testing philosophy →
Comparing Testing Approaches
Section titled “Comparing Testing Approaches”Scenarist fills a gap between unit tests and end-to-end tests. Each approach serves different purposes—they complement rather than replace each other:
- Unit tests verify individual functions and modules in isolation
- Scenarist verifies HTTP-level behavior with different external API scenarios
- E2E tests verify the complete user experience including browser interactions
For detailed comparisons with other tools (WireMock, Nock, Testcontainers, Playwright mocks), see our Tool Comparison guide. The comparison includes a decision guide to help you choose the right approach for your needs.
Limitations and Trade-offs
Section titled “Limitations and Trade-offs”HTTP only: Scenarist intercepts HTTP requests only. It cannot mock database calls, file system operations, or WebSocket connections (MSW supports WebSockets, but Scenarist’s scenario system is designed for HTTP request/response patterns). For apps with direct database access, see Testing Database Apps for recommended strategies.
Database parallelism: While Scenarist enables parallel HTTP tests via test ID isolation, database testing requires different strategies. We recommend the Repository Pattern, which provides the same test ID isolation model for database access. See Parallelism Options for all approaches and trade-offs.
Single-server deployment: Scenarist stores test ID to scenario mappings in memory. This works well for local development and single-instance CI environments. Load-balanced deployments would require additional state management.
Mock maintenance: Scenario definitions need updates when external APIs change. Scenarist doesn’t validate that mocks match real API contracts—this is a deliberate trade-off for test isolation and speed.
Learning curve: Understanding scenario definitions, test ID isolation, and the relationship between mocks and real backend code requires initial investment. The documentation and examples aim to reduce this learning time.
Getting Started
Section titled “Getting Started”Choose your framework to see specific installation and usage instructions:
Or explore core concepts that apply to all frameworks: