Skip to content

Generate a Jest replay session (the default happy path)

For Jest projects, softprobe generate jest-session is the recommended way to turn a captured case into a runnable replay test. It emits one TypeScript module that:

  • creates a replay-mode session,
  • loads the case file,
  • registers one findInCase + mockOutbound pair per outbound HTTP hop in the case,
  • returns { sessionId, close } for your test to consume.

You never hand-write the mock list. You never call the runtime's JSON API. You commit the generated module alongside the case file and regenerate after every capture refresh.

This is the flow docs/design.md §3.2 calls the "default happy path" — the 80/20 choice.

Prerequisites

  • A Softprobe runtime running somewhere (local: docker compose up in e2e/; CI: see CI integration).
  • A case file — produced by Capture your first session or shipped with the repo.
  • @softprobe/softprobe-js installed: npm install --save-dev @softprobe/softprobe-js.
  • The CLI on PATH: softprobe doctor prints version info.

One command

bash
softprobe generate jest-session \
  --case cases/checkout.case.json \
  --out test/generated/checkout.replay.session.ts
FlagPurpose
--casePath to the *.case.json file to replay.
--outWhere to write the generated TypeScript module. Convention: test/generated/<scenario>.replay.session.ts.

The CLI reads the case, walks its traces[], deduplicates (direction, method, path) outbound hops, and emits one module:

typescript
// Generated by `softprobe generate jest-session` - do not edit by hand.

import path from 'path';
import { Softprobe } from '@softprobe/softprobe-js';

const generatedDir = path.dirname(__filename);

export async function startReplaySession(): Promise<{ sessionId: string; close: () => Promise<void> }> {
  const softprobe = new Softprobe();
  const session = await softprobe.startSession({ mode: 'replay' });
  await session.loadCaseFromFile(path.join(generatedDir, '../../cases/checkout.case.json'));

  const hit0 = session.findInCase({
    direction: 'outbound',
    method: 'POST',
    path: '/v1/payment_intents',
  });
  await session.mockOutbound({
    direction: 'outbound',
    method: 'POST',
    path: '/v1/payment_intents',
    response: hit0.response,
  });

  const hit1 = session.findInCase({
    direction: 'outbound',
    method: 'GET',
    path: '/v1/customers/cus_abc',
  });
  await session.mockOutbound({
    direction: 'outbound',
    method: 'GET',
    path: '/v1/customers/cus_abc',
    response: hit1.response,
  });

  return {
    sessionId: session.id,
    close: () => session.close(),
  };
}

Anatomy:

  • Imports — only @softprobe/softprobe-js. No hand-rolled fetch to the runtime.
  • startReplaySession() — starts the session, loads the case (path is resolved relative to the generated module, so moving the file doesn't break it), and registers one findInCasemockOutbound pair per unique outbound hop.
  • Return value{ sessionId, close }. Attach sessionId to test requests via x-softprobe-session-id; call close() in afterAll.

Use it in a Jest test

typescript
// test/checkout.replay.test.ts
import fetch from 'node-fetch';
import { startReplaySession } from './generated/checkout.replay.session';

describe('checkout replay', () => {
  let sessionId: string;
  let close: () => Promise<void>;

  beforeAll(async () => {
    const session = await startReplaySession();
    sessionId = session.sessionId;
    close = session.close;
  });

  afterAll(async () => {
    await close();
  });

  it('returns a successful checkout response', async () => {
    const res = await fetch('http://127.0.0.1:8082/checkout', {
      method: 'POST',
      headers: {
        'x-softprobe-session-id': sessionId,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ cartId: 'c_test' }),
    });

    expect(res.status).toBe(200);
    expect(await res.json()).toMatchObject({ status: 'succeeded' });
  });
});

That's the entire test. No mocking library, no HTTP interception setup, no jest.mock(...).

Regeneration workflow

The generated module is a build artifact of the case file. Every time you re-capture (for instance, because an upstream API changed), regenerate:

bash
# 1. Re-capture
softprobe capture run --out cases/checkout.case.json -- npm run test:integration

# 2. Regenerate
softprobe generate jest-session \
  --case cases/checkout.case.json \
  --out test/generated/checkout.replay.session.ts

# 3. Review the diff
git diff cases/checkout.case.json test/generated/checkout.replay.session.ts

A Makefile target or an npm script keeps this one step:

json
{
  "scripts": {
    "replay:regen": "softprobe generate jest-session --case cases/checkout.case.json --out test/generated/checkout.replay.session.ts"
  }
}

Sidecar YAML (optional policy and fixtures)

The generator emits plain SDK calls. To layer policy or auth fixtures on top without editing the generated module, add a tiny wrapper next to it:

typescript
// test/generated/checkout.replay.session.wrapper.ts
import { startReplaySession } from './checkout.replay.session';

export async function startHardenedReplaySession() {
  const session = await startReplaySession();
  // Post-generation setup:
  // - strict policy
  // - additional fixtures
  // - one-off overrides
  return session;
}

The wrapper is yours to edit and commit; the generated module is regenerated.

Diff-review tips

When reviewing a regenerated module in code review:

  1. path changes usually mean the upstream API path or URL structure changed — investigate before merging.
  2. New findInCase / mockOutbound pairs mean the capture added a new outbound dependency — confirm that's expected.
  3. Removed pairs mean a call was removed — confirm your app no longer makes it, or a capture ran under a different scenario.
  4. Reorderings are harmless: the generator sorts by (path, method, direction) for stable diffs.

The case file diff is usually more informative than the module diff.

What the generator is not

  • It does not generate a Jest test wrapper — only the session helper. You still write the describe / it yourself (usually once, then never touch).
  • It does not emit rules for inbound traffic. If you need to mock inbound (rare — you're testing the real app), add a mockOutbound({ direction: 'inbound', ... }) call in your wrapper.
  • It does not handle consume: once semantics automatically. If your capture contains the same path multiple times with different responses, the generator keeps the first and skips duplicates; use hand-written mockOutbound for ordered response sequences.

When to drop down to ad-hoc findInCase + mockOutbound

Reach for Path B (ad-hoc) when you:

  • need to mutate a captured response (edit a timestamp, swap a test card, zero a field),
  • need ordered responses (first call 503, second call 200),
  • need predicate-based matching (hostSuffix, bodyJsonPath) beyond (direction, method, path) triples,
  • are writing a test that mixes real and mocked upstream calls.

See Replay in Jest for the ad-hoc pattern.

See also