Database testing
PostgreSQL, MySQL, MongoDB, Redis. Test real queries, migrations, transactions.
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.
Before diving into comparisons, here's what Scenarist brings to the table:
x-scenarist-test-id). No Docker, no separate processes, no complex network configuration┌─────────────────────────────────────────────────────────────────┐│ Your Application ││ ││ ┌─────────────────┐ ┌──────────────────────────┐ ││ │ │ │ │ ││ │ Database │ │ External APIs │ ││ │ PostgreSQL │ │ Stripe, Auth0, │ ││ │ Redis │ │ SendGrid, Twilio │ ││ │ MongoDB │ │ │ ││ │ │ │ │ ││ └────────┬────────┘ └────────────┬─────────────┘ ││ │ │ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────┐ ┌──────────────────────────┐ ││ │ Testcontainers │ │ Scenarist │ ││ │ │ │ │ ││ │ Real services │ │ Mocked responses │ ││ │ in containers │ │ Scenario-based │ ││ └─────────────────┘ └──────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘| Aspect | Testcontainers | Scenarist |
|---|---|---|
| Purpose | Run real services | Mock HTTP APIs |
| Use case | Databases, message queues | Third-party APIs |
| What runs | Actual software in Docker | Your app with mocked responses |
| Fidelity | Real behavior | Controlled scenarios |
| Startup time | Seconds to minutes | Instant |
| Resource usage | Container per service | In-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:
// Testcontainers for your databaseimport { PostgreSqlContainer } from "@testcontainers/postgresql";
const postgres = await new PostgreSqlContainer().withDatabase("testdb").start();
// Your app connects to real PostgreSQL in Dockerprocess.env.DATABASE_URL = postgres.getConnectionUri();
// Scenarist for external APIs// Stripe, SendGrid, etc. are mocked - they don't have containersexport 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 }, }, ], }, },});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:
| Module | Package | Status |
|---|---|---|
| MockServer | @testcontainers/mockserver | Official |
| WireMock | @wiremock/wiremock-testcontainers-node | Official Partner |
// Testcontainers with WireMock for HTTP mockingimport { 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 containerprocess.env.STRIPE_API_URL = container.getBaseUrl();Trade-offs vs Scenarist:
| Factor | Testcontainers + WireMock | Scenarist |
|---|---|---|
| Setup | Docker + container management | npm install |
| Startup time | Seconds (container spin-up) | Instant (in-process) |
| Test isolation | Per-container (heavier) | Per-header (lightweight) |
| Runtime switching | Admin API | Single API call |
| Network config | Required (proxy/env vars) | None (in-process) |
When HTTP mocking via Testcontainers makes sense:
When Scenarist is simpler:
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// Testcontainers gives you the real thingconst postgres = await new PostgreSqlContainer().start();
// Real queries against real PostgreSQLconst result = await db.query(` SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL`, ['test@example.com']);
// Real constraints, real behavior// Your ORM/query builder works exactly like productionRule of thumb:
| Factor | Testcontainers | Scenarist |
|---|---|---|
| Databases | ✓ Best choice | Not recommended |
| Message queues | ✓ Best choice | Not recommended |
| HTTP API mocking | ✓ Via WireMock/MockServer | ✓ Built-in |
| Setup complexity | Docker + containers | npm install |
| Startup time | Seconds (containers) | ✓ Instant |
| Resource usage | Higher (containers) | ✓ Minimal |
| Fidelity | ✓ Real behavior | Controlled |
| Parallel isolation | Per-container | ✓ Per-header (lightweight) |
| Runtime switching | Admin API | ✓ Single API call |
| Playwright fixtures | Manual 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:
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.