Mock an external dependency
Capture gives you real upstream bytes. Sometimes you want to override those bytes — simulate a failure mode, test against a value the upstream doesn't naturally produce, or mock a dependency that was never captured in the first place. This guide covers all three.
The three flavors of mocking
| You have… | You want… | Use |
|---|---|---|
| A captured response, happy path | Replay it verbatim | findInCase(...) → mockOutbound(..., response: hit.response) |
| A captured response, modified | Replay a variant of the capture | findInCase(...) → mutate → mockOutbound(...) |
| No capture at all | Hand-written mock | mockOutbound(..., response: { status, body }) without findInCase |
Happy-path replay (recap)
The default case from Replay in Jest:
const hit = session.findInCase({
direction: 'outbound',
hostSuffix: 'stripe.com',
pathPrefix: '/v1/payment_intents',
});
await session.mockOutbound({
direction: 'outbound',
hostSuffix: 'stripe.com',
pathPrefix: '/v1/payment_intents',
response: hit.response,
});Simulate a failure
You captured a successful Stripe call. Now you want to test "what if Stripe returns 429 Too Many Requests?" No need to re-capture — just hand-write the response:
await session.mockOutbound({
direction: 'outbound',
method: 'POST',
hostSuffix: 'stripe.com',
pathPrefix: '/v1/payment_intents',
response: {
status: 429,
headers: { 'retry-after': '2', 'content-type': 'application/json' },
body: JSON.stringify({
error: { type: 'rate_limit_error', message: 'Too many requests' },
}),
},
});Your test now asserts the app's backoff or error-handling behavior — a scenario that is nearly impossible to trigger against the real Stripe sandbox.
Simulate a slow upstream
Add a synthetic latency to the rule:
await session.mockOutbound({
direction: 'outbound',
hostSuffix: 'stripe.com',
response: hit.response,
latencyMs: 2500, // proxy delays 2.5s before returning the mock
});Useful for testing timeout and circuit-breaker logic without real-world flakes.
Mock an upstream you never captured
Not every upstream needs to have been captured to be mocked. mockOutbound with no findInCase works:
// No capture for this endpoint exists. Just hand-roll a plausible response.
await session.mockOutbound({
direction: 'outbound',
method: 'GET',
hostSuffix: 'geoip.example',
path: '/lookup',
response: {
status: 200,
body: JSON.stringify({ country: 'US', region: 'CA', city: 'San Francisco' }),
},
});The SDK sends a rule to the runtime; the proxy matches it on the next outbound request and returns that payload.
Override part of a captured response
const hit = session.findInCase({
direction: 'outbound',
hostSuffix: 'stripe.com',
pathPrefix: '/v1/payment_intents',
});
const body = JSON.parse(hit.response.body);
body.amount = 9900; // replay the capture, but charge less
body.receipt_email = 'test@example.com';
await session.mockOutbound({
direction: 'outbound',
hostSuffix: 'stripe.com',
pathPrefix: '/v1/payment_intents',
response: { ...hit.response, body: JSON.stringify(body) },
});Mocking only some dependencies while others pass through
A common integration setup: mock the paid external APIs, let the internal services stay real.
await session.setPolicy({
externalHttp: 'allow', // unmocked outbound goes through
});
// Mock only Stripe and Sendgrid
await session.mockOutbound({ hostSuffix: 'stripe.com', response: stripeHit.response });
await session.mockOutbound({ hostSuffix: 'sendgrid.net', response: sendgridHit.response });Or, invert the defaults — block everything unknown and allowlist a specific host:
await session.setPolicy({ externalHttp: 'strict' });
// Block everything by policy, but let one host through:
await session.mockOutbound({
hostSuffix: 'internal.svc.cluster.local',
then: { action: 'passthrough' },
priority: 500,
});Chain of calls: same upstream, different responses
Suppose the app calls /v1/order/$ID three times in a sequence and you want each call to return different data:
// Call 1: pending
await session.mockOutbound({
direction: 'outbound',
pathPrefix: '/v1/order/',
consume: 'once',
priority: 300,
response: { status: 200, body: JSON.stringify({ status: 'pending' }) },
});
// Call 2: processing
await session.mockOutbound({
direction: 'outbound',
pathPrefix: '/v1/order/',
consume: 'once',
priority: 200,
response: { status: 200, body: JSON.stringify({ status: 'processing' }) },
});
// Call 3: final
await session.mockOutbound({
direction: 'outbound',
pathPrefix: '/v1/order/',
consume: 'many',
priority: 100,
response: { status: 200, body: JSON.stringify({ status: 'completed' }) },
});consume: 'once' removes the rule after first match; priority orders the fallbacks. The last rule (consume: 'many') is the final state.
Mocking inbound traffic
Sometimes you want to test a webhook endpoint on your app against a specific inbound request the app returns data for. That's the same mockOutbound API with direction: 'inbound':
await session.mockOutbound({
direction: 'inbound',
method: 'POST',
path: '/webhooks/stripe',
response: {
status: 200,
body: JSON.stringify({ received: true, eventId: 'evt_test_123' }),
},
});Called from the proxy's ingress leg; returns the canned response without the app ever running for that request. Usually you don't need this (driving the real app handler is the point), but it's useful for testing ingress middleware in isolation.
Clearing rules between tests
mockOutbound calls within a session accumulate. Between test cases:
beforeEach(() => session.clearRules());This drops all session-local mocks but keeps the session and loaded case intact — fast reset.
Common mistakes
Predicate too broad. Writing { hostSuffix: 'stripe.com' } mocks every Stripe endpoint — the one for payments, the one for webhooks, the one for customers. Narrow to { pathPrefix: '/v1/payment_intents' } unless you really mean all of them.
Predicate too strict. Writing { path: '/v1/payment_intents/pi_abc123' } only matches that exact URL. If the ID varies between captures, use pathPrefix: '/v1/payment_intents'.
Headers mismatch in replay. The proxy returns your response.headers verbatim. If you omit content-type, downstream parsers may choke. When in doubt, echo hit.response.headers.
Body type mismatch. mockOutbound accepts either a string or a JSON-serializable object for response.body. If you pass an object, the SDK serializes with JSON.stringify — make sure the content-type header matches.
Next
- Rules and policy — the mental model beneath these calls.
- Write a hook — reusable transforms (PII masking, date rewriting) that apply to many rules.
- Run a suite at scale — the same patterns, driven from YAML.