Skip to content

Express Example App

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

This example app showcases all major Scenarist features with Express:

  • 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
  • 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
  • Node.js 20+
  • pnpm 9+
Terminal window
# Clone the repository
git clone https://github.com/citypaul/scenarist.git
cd scenarist
# Install dependencies
pnpm install
# Navigate to Express example
cd apps/express-example
Terminal window
# Start the Express server
pnpm dev

Server runs on http://localhost:3000.

Terminal window
# Run all tests
pnpm test
# Run specific test file
pnpm test scenario-switching
# Run with coverage
pnpm test:coverage

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 apps
export 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 point
const main = async () => {
const { app, scenarist } = await createApp();
scenarist?.start();
app.listen(3000);
};
main().catch(console.error);

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}}' }
}
}
]
}

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 variables
const 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");
});
});
  1. Middleware Registration - Scenarist middleware added to Express app
  2. Test ID Extraction - Middleware extracts x-scenarist-test-id header from requests
  3. Scenario Activation - Test calls POST /__scenario__ to set active scenario
  4. Request Handling - Express routes execute normally
  5. External API Interception - MSW intercepts external API calls
  6. Scenario Response - Returns response defined in scenario
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 Client
  • 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
// Middleware that fetches user from external API
app.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 scenarios
describe("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");
});
});
import type { ScenaristScenarios } from "@scenarist/express-adapter";
// Different responses based on request body
const 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;
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
});
});