Express Example App
Overview
Section titled “Overview”The Express example demonstrates HTTP-level integration testing for Express applications using Scenarist. This example focuses on testing middleware, route handlers, and external API integrations.
GitHub: apps/express-example
What It Demonstrates
Section titled “What It Demonstrates”This example app showcases all major Scenarist features with Express:
Core Features
Section titled “Core Features”- Middleware Testing - Test Express middleware chains with real HTTP requests
- Route Handlers - Test route handlers with different external API responses
- Test ID Isolation - Parallel test execution without interference
- Runtime Scenario Switching - Switch scenarios during test execution
Dynamic Response Features
Section titled “Dynamic Response Features”- Request Matching - Different responses based on request content
- Sequences - Multi-step processes (polling, async operations)
- Stateful Mocks - State capture and injection across requests
- Default Fallback - Graceful handling when no scenario is active
Installation
Section titled “Installation”Prerequisites
Section titled “Prerequisites”- Node.js 20+
- pnpm 9+
Clone and Install
Section titled “Clone and Install”# Clone the repositorygit clone https://github.com/citypaul/scenarist.gitcd scenarist
# Install dependenciespnpm install
# Navigate to Express examplecd apps/express-exampleRunning the Example
Section titled “Running the Example”Development Mode
Section titled “Development Mode”# Start the Express serverpnpm devServer runs on http://localhost:3000.
Run Tests
Section titled “Run Tests”# Run all testspnpm test
# Run specific test filepnpm test scenario-switching
# Run with coveragepnpm test:coverageKey Files
Section titled “Key Files”Scenarist Setup
Section titled “Scenarist Setup”src/server.ts - Express server with Scenarist integration
import express from "express";import { createScenarist, type ExpressScenarist,} from "@scenarist/express-adapter";import { scenarios } from "./scenarios";
// Use async factory pattern for Express appsexport const createApp = async () => { const app = express();
// Create Scenarist instance (async - returns Promise) const scenarist = createScenarist({ enabled: process.env.NODE_ENV === "test", scenarios, });
// Register Scenarist middleware (only if enabled) if (scenarist) { app.use(scenarist.middleware); }
// Your routes app.get("/api/user", async (req, res) => { const response = await fetch("https://api.auth.example.com/user"); const user = await response.json(); res.json(user); });
return { app, scenarist };};
// Entry pointconst main = async () => { const { app, scenarist } = await createApp(); scenarist?.start(); app.listen(3000);};
main().catch(console.error);Scenario Definitions
Section titled “Scenario Definitions”src/scenarios.ts - All scenario definitions (view on GitHub)
Key scenarios:
default - Standard responses for all tests
premiumUser - Premium tier user
premiumUser: { id: 'premiumUser', name: 'Premium User', mocks: [{ method: 'GET', url: 'https://api.auth.example.com/user', response: { status: 200, body: { id: 'user-123', tier: 'premium' } } }]}githubPolling - Polling sequence
githubPolling: { id: 'githubPolling', mocks: [{ method: 'GET', url: 'https://api.github.com/repos/user/repo/status', sequence: { responses: [ { status: 200, body: { status: 'pending' } }, { status: 200, body: { status: 'processing' } }, { status: 200, body: { status: 'complete' } } ], repeat: 'last' } }]}shoppingCart - Stateful shopping cart
shoppingCart: { id: 'shoppingCart', mocks: [ { method: 'POST', url: 'https://api.cart.example.com/add', captureState: { items: { from: 'body', path: 'productId' } }, response: { status: 201 } }, { method: 'GET', url: 'https://api.cart.example.com/items', response: { status: 200, body: { items: '{{state.items}}' } } } ]}Test Examples
Section titled “Test Examples”tests/scenario-switching.test.ts - Basic scenario switching
import { describe, it, expect, beforeAll, afterAll } from "vitest";import request from "supertest";import { createApp } from "../src/server";
// Factory function for test setup - no let variablesconst createTestSetup = async () => { const { app, scenarist } = await createApp(); return { app, scenarist };};
describe("Scenario Switching", () => { const testContext = createTestSetup();
beforeAll(async () => { const { scenarist } = await testContext; scenarist?.start(); });
afterAll(async () => { const { scenarist } = await testContext; scenarist?.stop(); });
it("should switch to premium user scenario", async () => { const { app } = await testContext; const testId = "test-123";
// Switch to premiumUser scenario await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId) .send({ scenario: "premiumUser" });
// Request uses premium scenario const response = await request(app) .get("/api/user") .set("x-scenarist-test-id", testId);
expect(response.body.tier).toBe("premium"); });});tests/test-id-isolation.test.ts - Parallel test isolation
describe("Test ID Isolation", () => { it("should isolate scenarios by test ID", async () => { // Test 1 uses premiumUser const testId1 = "test-1"; await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId1) .send({ scenario: "premiumUser" });
// Test 2 uses standardUser const testId2 = "test-2"; await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId2) .send({ scenario: "standardUser" });
// Requests are isolated const res1 = await request(app) .get("/api/user") .set("x-scenarist-test-id", testId1); expect(res1.body.tier).toBe("premium");
const res2 = await request(app) .get("/api/user") .set("x-scenarist-test-id", testId2); expect(res2.body.tier).toBe("standard"); });});tests/dynamic-sequences.test.ts - Polling with sequences
describe("Dynamic Sequences", () => { it("should progress through polling sequence", async () => { const testId = "test-polling";
await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId) .send({ scenario: "githubPolling" });
// First request: pending const res1 = await request(app) .get("/api/poll") .set("x-scenarist-test-id", testId); expect(res1.body.status).toBe("pending");
// Second request: processing const res2 = await request(app) .get("/api/poll") .set("x-scenarist-test-id", testId); expect(res2.body.status).toBe("processing");
// Third request: complete const res3 = await request(app) .get("/api/poll") .set("x-scenarist-test-id", testId); expect(res3.body.status).toBe("complete"); });});tests/stateful-scenarios.test.ts - State capture and injection
describe("Stateful Scenarios", () => { it("should capture and inject state", async () => { const testId = "test-cart";
await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId) .send({ scenario: "shoppingCart" });
// Add item - state captured await request(app) .post("/api/cart/add") .set("x-scenarist-test-id", testId) .send({ productId: "prod-1" });
// Get cart - state injected const response = await request(app) .get("/api/cart/items") .set("x-scenarist-test-id", testId);
expect(response.body.items).toContain("prod-1"); });});Architecture
Section titled “Architecture”How It Works
Section titled “How It Works”- Middleware Registration - Scenarist middleware added to Express app
- Test ID Extraction - Middleware extracts
x-scenarist-test-idheader from requests - Scenario Activation - Test calls
POST /__scenario__to set active scenario - Request Handling - Express routes execute normally
- External API Interception - MSW intercepts external API calls
- Scenario Response - Returns response defined in scenario
Middleware Flow
Section titled “Middleware Flow”Incoming Request ↓[Scenarist Middleware] - Extracts x-scenarist-test-id header ↓[Express Route] - Executes normally ↓[External API Call] - fetch('https://api.example.com/...') ↓[MSW Intercepts] - Checks active scenario for test ID ↓[Scenario Response] - Returns mocked response ↓[Express Route] - Continues with mocked data ↓Response to ClientFile Structure
Section titled “File Structure”Directoryapps/express-example/
Directorysrc/
- server.ts Express app with Scenarist
- scenarios.ts Scenario definitions
Directoryroutes/ Express routes
- …
Directorytests/
- scenario-switching.test.ts
- test-id-isolation.test.ts
- dynamic-sequences.test.ts
- dynamic-matching.test.ts
- stateful-scenarios.test.ts
- package.json
Common Patterns
Section titled “Common Patterns”Testing Middleware
Section titled “Testing Middleware”// Middleware that fetches user from external APIapp.use(async (req, res, next) => { const response = await fetch("https://api.auth.example.com/user"); req.user = await response.json(); next();});
// Test with different user scenariosdescribe("Auth Middleware", () => { it("should set premium user", async () => { const testId = "test-premium";
await request(app) .post("/__scenario__") .set("x-scenarist-test-id", testId) .send({ scenario: "premiumUser" });
const response = await request(app) .get("/api/profile") .set("x-scenarist-test-id", testId);
expect(response.body.user.tier).toBe("premium"); });});Testing with Request Matching
Section titled “Testing with Request Matching”import type { ScenaristScenarios } from "@scenarist/express-adapter";
// Different responses based on request bodyconst scenarios = { checkout: { id: "checkout", mocks: [ { method: "POST", url: "https://api.payment.example.com/charge", match: { body: { amount: 100 } }, response: { status: 200, body: { success: true } }, }, { method: "POST", url: "https://api.payment.example.com/charge", match: { body: { amount: 10000 } }, response: { status: 400, body: { error: "Amount too large" } }, }, ], },} as const satisfies ScenaristScenarios;Testing Default Fallback
Section titled “Testing Default Fallback”describe("Default Fallback", () => { it("should use default scenario when none specified", async () => { // No scenario switch - uses default const response = await request(app).get("/api/user");
expect(response.status).toBe(200); expect(response.body.tier).toBe("standard"); // default scenario });});Next Steps
Section titled “Next Steps”- Express Getting Started → - Integrate Scenarist into your Express app
- Request Matching → - Learn about request content matching
- Sequences → - Learn about response sequences
- Stateful Mocks → - Learn about state capture and injection