Skip to content

Scenarist vs WireMock

WireMock is a mature, widely-adopted mock server for simulating HTTP APIs. It runs as a standalone Java process and can be used from any language. Scenarist takes a different approach—intercepting requests within your Node.js process using MSW.

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
AspectScenaristWireMock
ArchitectureIn-process (MSW)Standalone server
LanguageTypeScript/JavaScriptLanguage-agnostic (Node.js via wiremock CLI or wiremock-captain client)
Test isolationPer-test via headerPer-server instance or Scenarios
Runtime switchingYes (single API call)Yes (Admin API)
Setupnpm installJAR, Docker, or npm + network configuration
Test framework integrationFirst-class Playwright fixturesGeneric HTTP API
Response sequencesBuilt-in (polling, state machines)Built-in (Scenarios feature)
Stateful mocksBuilt-in capture/inject per test IDBuilt-in state machine
RecordingNot supportedBuilt-in record/playback
TemplatingTemplate stringsHandlebars templating
// Mocks run in the same process as your app
import { createScenarist } from '@scenarist/express-adapter';
export const scenarist = createScenarist({
enabled: process.env.NODE_ENV === "test",
scenarios: {
default: {
mocks: [
{
url: "https://api.stripe.com/v1/charges",
method: "POST",
response: { status: 200, body: { id: "ch_123" } },
},
],
},
},
});
// No external process to manage
// No network latency between your app and mocks
// Debugging in the same process

Trade-off: Scenarist’s in-process approach means simpler setup—just npm install and configure. No separate server process, no network configuration to route your app’s requests. WireMock’s server approach works with any language (via JAR, Docker, or npm package) but requires running a separate process and configuring your app to route requests to the mock server.

// Each test gets isolated scenarios via test ID
test('payment success', async ({ page, switchScenario }) => {
// This test sees 'payment-success' responses
await switchScenario(page, 'payment-success');
await page.goto('/checkout');
await expect(page.locator('.success')).toBeVisible();
});
test('payment declined', async ({ page, switchScenario }) => {
// Same app, same time, different scenario
await switchScenario(page, 'payment-declined');
await page.goto('/checkout');
await expect(page.locator('.error')).toBeVisible();
});
// Both tests run in parallel against ONE server
// Test ID header routes to correct scenario

Trade-off: Scenarist’s test ID system was designed specifically for parallel test isolation. WireMock can achieve similar results but requires more infrastructure (multiple instances) or careful state management.

// Switch scenarios mid-test without restart
test('retry after failure', async ({ page, switchScenario }) => {
// Start with failure
await switchScenario(page, 'payment-timeout');
await page.goto('/checkout');
await page.click('[data-testid="submit"]');
await expect(page.locator('.retry-button')).toBeVisible();
// Switch to success - same test, same page
await switchScenario(page, 'payment-success');
await page.click('.retry-button');
await expect(page.locator('.success')).toBeVisible();
});

Trade-off: Both tools support runtime switching. Scenarist uses a single API call that automatically routes by test ID. WireMock’s Admin API is powerful but requires managing stub state and can be more complex in parallel test environments where you need to coordinate which test sees which stubs.

Scenarist provides dedicated Playwright fixtures that handle test ID generation, scenario switching, and header propagation automatically.

// tests/fixtures.ts - One-time setup
import { withScenarios, expect } from '@scenarist/playwright-helpers';
import { scenarios } from '../lib/scenarios';
export const test = withScenarios(scenarios);
export { expect };
// tests/checkout.spec.ts - Clean, type-safe tests
import { test, expect } from './fixtures';
test('premium user checkout', async ({ page, switchScenario }) => {
// Type-safe scenario ID with autocomplete
await switchScenario(page, 'premium-user');
await page.goto('/checkout');
await expect(page.locator('.premium-discount')).toBeVisible();
});
test('payment declined', async ({ page, switchScenario }) => {
await switchScenario(page, 'payment-declined');
await page.goto('/checkout');
await page.click('[data-testid="submit"]');
await expect(page.locator('.error-message')).toContainText('declined');
});

Trade-off: Scenarist’s Playwright helpers provide automatic test ID isolation—each test gets its own scenario state without manual header management. WireMock requires manual HTTP calls and careful state management. Future Scenarist releases plan to add similar first-class support for Cypress.

Both Scenarist and WireMock support dynamic responses, but with different approaches and trade-offs.

Response Sequences (Polling, State Machines)

Section titled “Response Sequences (Polling, State Machines)”
// Built-in sequence support
{
method: 'GET',
url: 'https://api.example.com/job/:id/status',
sequence: {
responses: [
{ status: 200, body: { status: 'pending' } },
{ status: 200, body: { status: 'processing' } },
{ status: 200, body: { status: 'complete', result: 'success' } }
],
repeat: 'last' // Stay at final response
}
}
// Perfect for testing polling UIs
test('shows job progress', async ({ page, switchScenario }) => {
await switchScenario(page, 'job-processing');
await page.goto('/jobs/123');
await expect(page.locator('.status')).toContainText('pending');
await page.click('[data-testid="refresh"]');
await expect(page.locator('.status')).toContainText('processing');
await page.click('[data-testid="refresh"]');
await expect(page.locator('.status')).toContainText('complete');
});

Trade-off: Both tools have built-in sequence/state machine support. Scenarist’s approach is declarative and isolated per test ID. WireMock’s Scenarios are powerful state machines but the state is global to the WireMock instance—parallel tests need separate instances or careful coordination.

// Declarative matching with body, headers, query, regex
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
match: {
body: {
amount: 5000,
currency: 'usd',
metadata: { orderId: /^order-\d+$/ } // Regex support
},
headers: {
'idempotency-key': /.+/ // Must be present
}
},
response: { status: 200, body: { id: 'ch_premium_123' } }
}
// Specificity-based selection - most specific match wins
// No need to carefully order your mocks
// Capture state from requests, inject into responses
const scenarios = {
'cart-flow': {
mocks: [
// Capture item when added to cart
{
method: 'POST',
url: '*/api/cart/items',
captureState: {
'cartItems[]': 'body.item' // Append to array
},
response: { status: 201, body: { success: true } }
},
// Inject captured state into response
{
method: 'GET',
url: '*/api/cart',
response: {
status: 200,
body: {
items: '{{state.cartItems}}',
count: '{{state.cartItems.length}}'
}
}
}
]
}
};
// Test multi-step flows with stateful behavior
test('cart accumulates items', async ({ page, switchScenario }) => {
await switchScenario(page, 'cart-flow');
await page.goto('/products');
await page.click('[data-product="widget"]');
await page.click('[data-product="gadget"]');
await page.goto('/cart');
await expect(page.locator('.cart-count')).toContainText('2');
});

Trade-off: WireMock’s Scenarios handle state transitions (tracking which state you’re in). Scenarist’s state capture goes further—capturing arbitrary data from requests and injecting it into subsequent responses—with per-test-ID isolation for parallel tests. WireMock can achieve data capture via Handlebars templating but state is global to the instance.

Simple setup

Just npm install and configure scenarios. No separate server process or network configuration required.

TypeScript-first development

Scenarios are TypeScript objects with full type safety. IDE autocomplete, compile-time errors, refactoring support.

Parallel test isolation

Built-in test ID system lets hundreds of tests run simultaneously with different scenarios—no separate instances needed.

First-class Playwright support

Dedicated fixtures with automatic test ID handling. Each test gets its own scenario state with type-safe switching.

Per-test-ID state isolation

Response sequences and stateful mocks are isolated per test ID—parallel tests never share state.

Next.js multi-process handling

Next.js has

documented singleton issues

that break MSW. Scenarist’s adapter includes built-in globalThis guards—one stable MSW instance regardless of how Next.js loads modules.

Polyglot environment

Your team uses multiple languages (Java, Python, .NET). WireMock works with any HTTP client.

Record and playback

Need to capture real API interactions and replay them. WireMock’s recording is battle-tested.

Existing WireMock investment

Team already knows WireMock, has existing stub libraries, or uses WireMock Cloud.

Contract testing

Using WireMock for contract testing with Spring Cloud Contract or similar frameworks.

If you’re considering migrating from WireMock:

  1. Stub definitions translate directly — WireMock JSON mappings map cleanly to Scenarist scenarios
  2. Test isolation improves — No more managing multiple WireMock instances for parallel tests
  3. Recording doesn’t migrate — You’ll lose record/playback capabilities
// WireMock JSON
{
"request": {
"method": "POST",
"url": "/v1/charges",
"bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
},
"response": {
"status": 200,
"jsonBody": { "id": "ch_123", "status": "succeeded" }
}
}
// Equivalent Scenarist scenario
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
match: { body: { amount: /.*/ } },
response: {
status: 200,
body: { id: 'ch_123', status: 'succeeded' }
}
}

You can use WireMock and Scenarist together:

  • WireMock for services your team doesn’t own (microservices from other teams)
  • Scenarist for third-party APIs you need to test scenarios for (Stripe, Auth0)

Point your app at WireMock for internal services while Scenarist mocks external APIs.

FactorScenaristWireMock
Setup simplicity✓ npm installJAR/Docker/npm + network config
TypeScript integration✓ NativeVia wiremock-captain client
Parallel test isolation✓ Header-basedMultiple instances
Runtime switching✓ Single API callAdmin API
Playwright integration✓ First-class fixturesManual HTTP calls
Response sequences✓ Built-in✓ Built-in (Scenarios)
Stateful mocks✓ Per-test-ID isolation✓ Global state machine
Next.js multi-processBuilt-in singleton guardsManual workarounds needed
Language supportJavaScript/TypeScript✓ Any language
Record/playbackNot supported✓ Built-in
Ecosystem maturityNewer✓ Battle-tested
Contract testingNot supported✓ Spring Cloud Contract

Bottom line: For Node.js projects, Scenarist keeps you in a single ecosystem—TypeScript scenarios, npm dependencies, Playwright fixtures, no context-switching to Java or Docker. Choose Scenarist when you value in-process simplicity, header-based parallel test isolation, and first-class Playwright support. Choose WireMock when you’re in a polyglot environment, need recording, contract testing, or when your team already has WireMock expertise. Both tools are capable—the choice is primarily about staying in your ecosystem vs. language flexibility.