Skip to content

Scenarist vs Testcontainers

Testcontainers runs real services in Docker containers for testing—PostgreSQL, Redis, Kafka, and more. Scenarist mocks HTTP APIs. These tools solve different problems and are often complementary rather than competing.

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 Application │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ │ │ │ │
│ │ Database │ │ External APIs │ │
│ │ PostgreSQL │ │ Stripe, Auth0, │ │
│ │ Redis │ │ SendGrid, Twilio │ │
│ │ MongoDB │ │ │ │
│ │ │ │ │ │
│ └────────┬────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ Testcontainers │ │ Scenarist │ │
│ │ │ │ │ │
│ │ Real services │ │ Mocked responses │ │
│ │ in containers │ │ Scenario-based │ │
│ └─────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
AspectTestcontainersScenarist
PurposeRun real servicesMock HTTP APIs
Use caseDatabases, message queuesThird-party APIs
What runsActual software in DockerYour app with mocked responses
FidelityReal behaviorControlled scenarios
Startup timeSeconds to minutesInstant
Resource usageContainer per serviceIn-process

Database testing

PostgreSQL, MySQL, MongoDB, Redis. Test real queries, migrations, transactions.

Message queues

Kafka, RabbitMQ, SQS (LocalStack). Test actual message processing.

Infrastructure

Elasticsearch, MinIO, Keycloak. Test integrations with real services.

Contract validation

Verify your code works with the actual database version you’ll deploy.

External HTTP APIs

Stripe, Auth0, SendGrid, Twilio. APIs you don’t control and can’t run locally.

Error scenarios

Test how your app handles API timeouts, rate limits, specific error codes.

Parallel testing

Run many tests simultaneously with different API states.

Edge cases

Scenarios you can’t easily create with real services.

A typical application might use both tools:

test-setup.ts
// Testcontainers for your database
import { PostgreSqlContainer } from "@testcontainers/postgresql";
const postgres = await new PostgreSqlContainer().withDatabase("testdb").start();
// Your app connects to real PostgreSQL in Docker
process.env.DATABASE_URL = postgres.getConnectionUri();
// Scenarist for external APIs
// Stripe, SendGrid, etc. are mocked - they don't have containers
export const scenarist = createScenarist({
enabled: true,
scenarios: {
default: {
mocks: [
{
url: "https://api.stripe.com/v1/charges",
method: "POST",
response: { status: 200, body: { id: "ch_123" } },
},
{
url: "https://api.sendgrid.com/v3/mail/send",
method: "POST",
response: { status: 202 },
},
],
},
},
});
checkout.spec.ts
test("creates order and sends confirmation", async ({
page,
switchScenario,
}) => {
// Database operations use real PostgreSQL (Testcontainers)
// Stripe payment uses mocked API (Scenarist)
// SendGrid email uses mocked API (Scenarist)
await switchScenario(page, "payment-success");
await page.goto("/checkout");
await page.fill('[name="card"]', "4242424242424242");
await page.click("#submit");
// Real database write happened
// Mocked Stripe returned success
// Mocked SendGrid accepted email
await expect(page.locator(".success")).toBeVisible();
// Verify database state (real PostgreSQL)
const order = await db.query("SELECT * FROM orders WHERE id = $1", [orderId]);
expect(order.status).toBe("completed");
});

Yes, but with more complexity. Testcontainers has modules for HTTP mock servers:

ModulePackageStatus
MockServer@testcontainers/mockserverOfficial
WireMock@wiremock/wiremock-testcontainers-nodeOfficial Partner
// Testcontainers with WireMock for HTTP mocking
import { WireMockContainer } from "@wiremock/wiremock-testcontainers-node";
const container = await new WireMockContainer()
.withMapping({
request: { method: "GET", urlPath: "/v1/products" },
response: { status: 200, jsonBody: { products: [] } },
})
.start();
// Point your app at the container
process.env.STRIPE_API_URL = container.getBaseUrl();

Trade-offs vs Scenarist:

FactorTestcontainers + WireMockScenarist
SetupDocker + container managementnpm install
Startup timeSeconds (container spin-up)Instant (in-process)
Test isolationPer-container (heavier)Per-header (lightweight)
Runtime switchingAdmin APISingle API call
Network configRequired (proxy/env vars)None (in-process)

When HTTP mocking via Testcontainers makes sense:

  • You’re already heavily invested in Testcontainers
  • You want identical mocking approach to your Java/Go services
  • You need WireMock’s specific features (recording, contract testing)

When Scenarist is simpler:

  • You want the lightest possible setup
  • You’re already in a Node.js/TypeScript environment
  • You value instant test startup over container isolation

Mocking databases is possible but loses fidelity:

// You could mock database API calls...
{
url: 'http://localhost:5432/query',
match: { body: { sql: /SELECT.*FROM users/ } },
response: {
body: { rows: [{ id: 1, email: 'test@example.com' }] }
}
}
// But you lose:
// - Real SQL query execution
// - Database constraints
// - Transaction behavior
// - Migration testing
// - Performance characteristics

Rule of thumb:

  • Use Testcontainers for services you own or can run locally
  • Use Scenarist for services you don’t control (third-party APIs)
FactorTestcontainersScenarist
Databases✓ Best choiceNot recommended
Message queues✓ Best choiceNot recommended
HTTP API mocking✓ Via WireMock/MockServer✓ Built-in
Setup complexityDocker + containersnpm install
Startup timeSeconds (containers)✓ Instant
Resource usageHigher (containers)✓ Minimal
Fidelity✓ Real behaviorControlled
Parallel isolationPer-container✓ Per-header (lightweight)
Runtime switchingAdmin API✓ Single API call
Playwright fixturesManual setup✓ First-class support

For comprehensive testing, use both:

┌─────────────────────────────────────────────────────────────────┐
│ Test Suite │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Your Application │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Database │ │ External APIs │ │ │
│ │ │ Layer │ │ Layer │ │ │
│ │ └──────┬───────┘ └────────┬─────────┘ │ │
│ └───────────┼─────────────────────────────┼───────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────┐ ┌────────────────────────────┐ │
│ │ Testcontainers │ │ Scenarist │ │
│ │ │ │ │ │
│ │ • PostgreSQL │ │ • Stripe (mocked) │ │
│ │ • Redis │ │ • Auth0 (mocked) │ │
│ │ • Kafka │ │ • SendGrid (mocked) │ │
│ │ │ │ │ │
│ │ Real services │ │ Controlled scenarios │ │
│ │ Real behavior │ │ Parallel isolation │ │
│ └───────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

This hybrid approach gives you:

  • Real database behavior (Testcontainers)
  • Controlled external API scenarios (Scenarist)
  • Comprehensive test coverage
  • Fast parallel execution (Scenarist’s test ID isolation)

See our Testcontainers Hybrid guide for detailed setup instructions.

Testcontainers and Scenarist solve different problems. Use Testcontainers for services you can run locally (databases, queues). Use Scenarist for services you can’t control (third-party APIs). For many applications, you’ll use both together.