Scenarist scripts
- Third-party HTTP APIs: Stripe, Auth0, SendGrid
- Other services your server calls over HTTP
- Any outgoing HTTP request from your server code
When testing applications that access multiple data sources (HTTP APIs + databases), the key insight is simple:
Use a single identifier to partition ALL data sources.
┌─────────────────────────────────────────────────────────┐│ Test Runner │├─────────────────────────────────────────────────────────┤│ Test A (x-scenarist-test-id: abc-123) ││ │ ││ ├─→ HTTP API ──→ Scenarist ──→ Mocks for "abc-123" ││ │ ││ └─→ Database ──→ Your Code ──→ Data for "abc-123" │├─────────────────────────────────────────────────────────┤│ Test B (x-scenarist-test-id: xyz-789) ││ │ ││ ├─→ HTTP API ──→ Scenarist ──→ Mocks for "xyz-789" ││ │ ││ └─→ Database ──→ Your Code ──→ Data for "xyz-789" │└─────────────────────────────────────────────────────────┘
Both tests run in PARALLEL with completely isolated state.This is the same pattern used in distributed systems for request tracing, multi-tenancy, and session management. The test ID is just another form of correlation ID.
| Responsibility | Tool | What It Does |
|---|---|---|
| HTTP API mocking | Scenarist | Intercepts fetch calls, returns mocks based on test ID |
| Database state isolation | Your code | Partitions database queries/data based on test ID |
Scenarist scripts
Scenarist leaves alone
fs.readFile and similarThere are multiple valid approaches to partition database state by test ID. Each has different trade-offs:
Abstract database access behind interfaces, inject test implementations that partition data by test ID.
Best for: Fast parallel tests, clean architecture, any ORM
Trade-offs: Requires refactoring database access; doesn’t test real SQL
Seed database directly in test setup, using test ID to isolate data.
test('premium user sees discounts', async ({ page }) => { const testId = generateTestId();
// Seed directly (if tests have DB access) await db.users.create({ testId, tier: 'premium', // ... });
// Or seed via HTTP endpoint await page.request.post('/test/seed', { headers: { 'x-scenarist-test-id': testId }, data: { scenarioId: 'premiumUser' } });
await page.goto('/products'); // ...});Best for: When tests have direct database access (e.g., same process)
Trade-offs: Tests coupled to database schema; requires cleanup logic
Add test_id column to tables, filter all queries automatically.
-- PostgreSQL RLS policyCREATE POLICY test_isolation ON users USING (test_id = current_setting('app.test_id'));Best for: When you can’t change application code
Trade-offs: Schema changes in production; RLS overhead
Pre-seed database with known state, reset between tests.
Best for: Testing specific SQL behavior; integration tests
Trade-offs: Slower; harder to parallelize
For teams that want scalable parallel testing of real server-side code, we recommend the repository pattern because:
Full Repository Pattern Guide →
Scenarist intercepts HTTP requests via MSW because every HTTP library eventually calls the same underlying APIs. Databases have no equivalent universal interception point.
Each ORM and driver is a different API surface:
prisma.user.findMany()db.select().from(users)userRepository.find()pg.query('SELECT * FROM users')// ❌ Scenarist CANNOT mock this - no HTTP requestexport async function fetchProducts() { return await db.products.findMany(); // Direct database call}That’s why you implement database isolation, using whatever approach fits your architecture. The examples show one way; adapt it to your needs.
| Your Situation | Approach |
|---|---|
| App only calls external HTTP APIs (no database) | Scenarist alone; no database strategy needed |
| Want scalable parallel tests + clean architecture | Repository Pattern |
| Tests have direct database access | Test fixtures with direct seeding |
| Can’t change application code | Row-level security or test ID columns |
| Need to test actual SQL queries, constraints | Testcontainers |
| Small test suite, prototyping | Sequential Execution |