Skip to content

Why Scenarist?

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 ApproachYour CodeExternal APIsBest For
Unit TestsMockedMockedIsolated function logic
Scenario-Based TestsRealMockedApplication behavior with controlled dependencies
End-to-End TestsRealRealFull 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.


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

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.

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.

  • 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 executes
export 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.

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 API
const stripe = await fetch("https://api.stripe.com/v1/products");
// ✅ WORKS - Different port on localhost
const products = await fetch("http://localhost:3001/products");
// ❌ Does NOT work - Same host/port internal route
const 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.

  • 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.

Learn how it works →

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.

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 →

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.

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.

Choose your framework to see specific installation and usage instructions:

Or explore core concepts that apply to all frameworks: