Skip to content

Scenarist + MSW

Scenarist is built on top of Mock Service Worker (MSW)—we don’t compete with MSW, we extend it. Understanding this relationship helps you decide when to use MSW directly vs when Scenarist’s scenario layer adds value.

What Scenarist Offers

Before diving into comparisons, here's what Scenarist brings to the table:

  • 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
┌─────────────────────────────────────────────┐
│ Your Test Suite │
│ ┌───────────────────────────────────────┐ │
│ │ Scenarist │ │
│ │ • Scenario management │ │
│ │ • Test ID isolation │ │
│ │ • Runtime switching │ │
│ │ • Framework adapters │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ MSW │ │ │
│ │ │ • Request interception │ │ │
│ │ │ • Network-level mocking │ │ │
│ │ │ • Handler matching │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

MSW provides the foundation:

  • Intercepts HTTP/HTTPS requests at the network level
  • Works with any HTTP client (fetch, axios, got, etc.)
  • Proven, battle-tested, actively maintained

Scenarist adds the testing layer:

  • Scenario management (group mocks by test case, switch at runtime)
  • Complete test ID isolation (scenarios, sequences, and captured state—all per test ID)
  • Declarative scenario definitions (inspectable, composable, type-safe)
  • Framework adapters (Express, Next.js integration out of the box)
  • First-class Playwright fixtures (automatic test ID handling)

Note: MSW v2.2.0+ added server.boundary() for isolating handler registrations in concurrent tests. Scenarist’s test ID system provides broader isolation—including sequence positions and captured state—and works with any test framework.

Storybook/Development

Mocking APIs during development or Storybook stories. No test isolation needed.

Single-Test Mocking

Simple tests where each test defines its own handlers inline. No scenario sharing.

API Design/Prototyping

Building frontend before backend exists. Interactive development, not testing.

Maximum Control

Need low-level MSW features like request timing, custom resolvers, or WebSocket mocking. (Scenarist focuses on HTTP request/response patterns.)

// src/mocks/handlers.ts - MSW for development
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({
id: 'user-123',
name: 'Development User',
email: 'dev@example.com'
});
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'test@example.com') {
return HttpResponse.json({ token: 'mock-jwt-token' });
}
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
})
];
// Use in browser with setupWorker for Storybook
// Use in Node with setupServer for development server

This is a great use of MSW directly. No test isolation needed, no scenario switching—just consistent mock data for development.

Parallel Testing

Multiple tests running simultaneously need different API states. Test ID isolation is essential.

Scenario Libraries

Reusable scenarios across test suites. “payment-success”, “payment-declined”, “auth-timeout” used everywhere.

Runtime Switching

Tests that change scenarios mid-execution. Retry flows, state transitions, multi-step user journeys.

Framework Integration

Testing Next.js Server Components, Express middleware, or other server-side code with scenarios.

// scenarios/payment.ts - Scenarist for testing
const scenarios = {
default: {
mocks: [{ url: 'https://api.stripe.com/v1/charges', response: { status: 200, body: { id: 'ch_123' } } }]
},
'payment-success': {
mocks: [{ url: 'https://api.stripe.com/v1/charges', response: { status: 200, body: { id: 'ch_456', status: 'succeeded' } } }]
},
'payment-declined': {
mocks: [{ url: 'https://api.stripe.com/v1/charges', response: { status: 402, body: { error: { code: 'card_declined' } } } }]
},
'payment-timeout': {
mocks: [{ url: 'https://api.stripe.com/v1/charges', response: { status: 504, delay: 30000 } }]
}
} as const satisfies ScenaristScenarios;
// payment.spec.ts - Tests run in parallel
test('shows success message', async ({ page, switchScenario }) => {
await switchScenario(page, 'payment-success');
// Test isolation via x-test-id header
});
test('shows decline message', async ({ page, switchScenario }) => {
await switchScenario(page, 'payment-declined');
// Different scenario, same time, same server
});
test('shows timeout message', async ({ page, switchScenario }) => {
await switchScenario(page, 'payment-timeout');
// Third scenario, all running in parallel
});

This is where Scenarist shines. Multiple tests, different scenarios, running simultaneously against one server instance.

MSW is excellent at intercepting requests. But as your test suite grows, you face challenges:

MSW v2.2.0+ introduced server.boundary() to help with parallel test isolation by scoping handler registrations. However, Scenarist takes a different approach with broader isolation.

// MSW v2.2.0+ - server.boundary() isolates handler registrations
import { server } from './mocks/server';
test.concurrent('Test A', server.boundary(async () => {
server.use(http.post('/api/payment', () =>
HttpResponse.json({ status: 'succeeded' })
));
// Handler only visible within this boundary
}));
test.concurrent('Test B', server.boundary(async () => {
server.use(http.post('/api/payment', () =>
HttpResponse.json({ status: 'failed' })
));
// Different handler, isolated from Test A
}));
// ✅ Handler registration is isolated
// ⚠️ Other state (sequences, captured data) is NOT isolated

Key difference: server.boundary() isolates handler registrations (which server.use() calls are visible). Scenarist isolates everything—the active scenario, sequence positions, and captured state—all keyed by test ID.

payment.test.ts
// Problem: Duplicating handler setup across tests
beforeEach(() => {
server.use(
http.post('/api/payment', () => HttpResponse.json({ status: 'succeeded' })),
http.get('/api/user', () => HttpResponse.json({ tier: 'premium' }))
);
});
// checkout.test.ts - same setup duplicated
beforeEach(() => {
server.use(
http.post('/api/payment', () => HttpResponse.json({ status: 'succeeded' })),
http.get('/api/user', () => HttpResponse.json({ tier: 'premium' }))
);
});
// What if payment API response format changes?
// Update everywhere!
// Problem: Changing handlers mid-test
test('retry after failure', async () => {
// Set up failure
server.use(http.post('/api/payment', () => HttpResponse.error()));
await page.click('#submit');
await expect(page.locator('.retry')).toBeVisible();
// Need to change to success...
server.resetHandlers(); // Affects other parallel tests!
server.use(http.post('/api/payment', () => HttpResponse.json({ status: 'ok' })));
await page.click('.retry');
});

You can use MSW directly for some use cases while using Scenarist for testing:

// src/mocks/browser.ts - MSW for Storybook
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// scenarios/index.ts - Scenarist for tests
export const scenarios = {
default: { mocks: [...] },
'edge-case': { mocks: [...] }
} as const satisfies ScenaristScenarios;

They don’t conflict. MSW handles development/Storybook. Scenarist handles your test suite with scenario management.

Use CaseMSW DirectlyScenarist
Development mocking
Storybook
API prototyping
Simple single-test mocks
Handler isolation (concurrent tests)✓ (server.boundary)✓ (test ID)
Complete state isolation (sequences, captured data)
Scenario libraries
Runtime scenario switching
Framework adapters
Playwright fixtures

Bottom line: MSW is the foundation for all request interception. MSW v2.2.0+ added server.boundary() for handler isolation in concurrent tests. Use MSW directly for development mocking or simple test scenarios. Use Scenarist when you need scenario management—complete per-test-ID isolation (including sequences and state), runtime switching, and reusable scenario libraries with first-class Playwright support.