Clones

API mocking vs stateful service clones

Both replace a third-party API in tests. They are not interchangeable. A mock returns what you told it to; a clone behaves the way the vendor does. Knowing which problem you’re solving decides which one you need.

Pratik Rana 9 min read

01Two tools that look similar and aren’t

An HTTP mock (WireMock, MSW, a stubbed fetch) intercepts requests and plays back canned responses. It’s fast, cheap, and perfectly suited to unit-level work. A stateful service clone is a different animal: it re-implements a vendor’s actual behavior — resource lifecycle, ID conventions, error codes, webhook delivery, signature schemes — behind the same endpoints, so your application can run against it unmodified for entire end-to-end flows.

The confusion happens because both show up as “the Stripe call didn’t hit real Stripe.” The differences show up in what your tests can assert. Five dimensions matter: fidelity, statefulness, webhooks, maintenance cost, and blast radius.

02Fidelity

A mock’s fidelity is exactly one response deep. You recorded { "id": "pi_123", "status": "succeeded" }, so every payment succeeds — until you need the failure path, then you record another fixture. Real vendors have combinatorial behavior: idempotency-key replays return the original result, card errors arrive as structured declines with specific codes, partial captures change subsequent refund semantics. Encoding all of that in fixtures means hand-writing a simulator anyway, badly.

A clone encodes vendor semantics once, in code: request an idempotent charge twice and you get the same payment intent; use a test decline card and you get the real decline payload shape. Your assertions stop checking “our handler ran” and start checking “our handler handled the thing the vendor actually sends.” That distinction is the whole difference between unit confidence and end-to-end confidence.

03Statefulness

Mocks are amnesiac by design. Ask a mock “did the customer I created three steps ago already exist?” and it shrugs — every request is answered from the script, with no memory between them. Multi-step flows collapse under this: create customer → attach card → charge → refund requires either one giant canned response or brittle sequencing logic layered on top of the mocker.

// Against a clone, state accumulates like the real service:
const cust = await stripe.customers.create({ email });
await stripe.paymentIntents.create({ customer: cust.id, amount: 4200, ... });
const list = await stripe.paymentIntents.list({ customer: cust.id });
expect(list.data).toHaveLength(1);   // true because the clone remembers

// Against a mock, this assertion is fiction — you scripted the list response.

Statefulness also buys you deterministic IDs and snapshot/restore: seed a known world, run the flow, reset between tests. That combination is what makes suites parallel-safe instead of serial-and-hopeful — see test data management.

04Webhooks and signatures

Half of a modern integration is inbound: Stripe webhooks firing on payment success, Twilio status callbacks, GitHub delivery events. Mocks handle this awkwardly — most teams end up calling their own webhook endpoint directly with a hand-built payload, which bypasses signature verification entirely and tests a fantasy shape.

A clone emits webhooks as part of normal operation: the charge succeeds because your app created it, the callback fires because the charge succeeded, and it arrives correctly signed so your verification middleware runs for real. You can also inject edge events (retries, out-of-order deliveries) deliberately:

If your webhook handler has never been tested against a replayed event with a valid signature, you don’t know whether your idempotency guard works. That single gap causes duplicate charges in production more often than any UI bug.

This is also where clones beat even live sandboxes: real vendor sandboxes deliver webhooks when they feel like it, through the public internet. A local clone delivers them deterministically, in-process.

05Maintenance drift

Mocks rot silently. Vendors add fields, deprecate endpoints, and change error taxonomies; your fixtures keep returning 2019-shaped responses and nothing tells you. The failure mode isn’t a red test — it’s a green suite guarding code that no longer matches reality, discovered during an incident.

Clones drift too, but controllably: the divergence lives in one implementation per vendor, which you update when the vendor changes — and a thin contract check against the live API (run weekly, manually triggered) surfaces drift immediately. With mocks, the divergence is smeared across hundreds of fixture files owned by nobody. For a deeper treatment of the safety question, see testing third-party APIs safely.

06Blast radius

Blast radius is about what breaks when the substitute is wrong. A wrong mock fails your unit test — cheap, contained, fixable in seconds. But mocks used beyond their station fail outward: a checkout E2E passing against scripted responses while the real integration is broken ships straight to production, because everyone trusted the green build.

Clones invert this. Their blast radius is bounded by design — no money moves, no emails leave, no SMS sends — while their behavior stays close enough to production that a green E2E suite actually means something. That’s the trade worth making at the system boundary. Molar ships clones for Stripe, Twilio, email, auth, S3, and GitHub as one coordinated set, wired into its payment-flow testing and CI gating so the substitution is automatic rather than per-project plumbing.

07The verdict: mocks for units, clones for paths

The honest answer is that these solve different problems, and mature teams use both:

  • Use mocks for pure business logic: pricing rules, retry policies, form validation. Speed dominates; the dependency is incidental.
  • Use clones anywhere the interaction itself matters: checkout, OTP verification, file storage, OAuth. Semantics dominate; a fake that doesn’t behave is worse than none.
  • Keep one thin live-contract check against each vendor to catch drift — scheduled, not blocking, never in the hot PR loop.

If you’re evaluating substitutes for a specific tool, our comparisons of WireMock-style mockers cover where static stubs top out. And if your flows span several vendors at once — pay, notify, verify — a coordinated clone set beats stitching five mockers together, as the recipes in Twilio OTP testing and email workflow testing demonstrate.

Stop choosing between fake and flaky

Molar’s stateful clones speak real vendor APIs with deterministic behavior — payments, SMS, email, auth — so your E2E tests mean something without touching production services.