01Sandboxes aren’t production, and that’s the problem
Vendor sandboxes exist to help you integrate, not to absorb thousands of automated test runs a day. Three failure modes show up on nearly every team we talk to:
- No behavioral parity. Test mode happily accepts the magic test card, but it won’t decline based on velocity, won’t surface 3DS challenges the same way, and rarely fires the dispute or refund webhooks your code actually has to handle. You pass in the sandbox and fail in production.
- Shared rate limits. Your CI queue isn’t the only tenant hammering a sandbox. When a vendor throttles test traffic globally, your pipeline turns red for reasons that have nothing to do with your code — a special circle of flaky-test hell documented at length in our piece on flaky tests and their fixes.
- Fake-data drift. Sandboxes accumulate months of other integrators’ test objects. Search endpoints return strangers’ records, idempotency keys collide, and list responses grow until pagination assumptions quietly break. We cover the cleanup side of this in test data management.
None of this means sandboxes are bad — they’re just not designed as test infrastructure. Treating them like infrastructure is how teams end up with suites that are simultaneously slow, flaky, and shallow.
02The live-key rules that keep you safe
Some teams skip the sandbox entirely and point tests at production APIs with real keys. Don’t — unless you enforce hard guardrails. The minimum set:
- Refuse live credentials in CI. Fail the build at startup if any configured key matches a live prefix (
sk_live_, a production Twilio SID, a bucket without-test). - Allow-list test cards and numbers. Only known-inert values may appear in fixtures.
- Cap spend and side effects. Hard ceilings per run, checked before each call.
- Separate accounts entirely. A dedicated sub-account for tests, so even a mistake can’t touch customer data.
03Mocks are fast; clones are honest
The usual fix is to mock the third party. HTTP-level mocks (WireMock, MSW, stub servers) are fast and
deterministic, but they only know what you taught them — which is usually the happy path from the docs.
They can’t tell you that your retry loop breaks on Stripe’s real idempotency_error shape,
or that your webhook handler chokes on event ordering.
A stateful clone sits between the extremes: it speaks the vendor’s API contract and keeps state across calls. Create a customer, charge them, refund them — each step sees the effects of the last, with deterministic IDs and realistic error semantics, but no money moves and no email leaves. We compare the tradeoffs in detail in API mocking vs. service clones, and walk through payments specifically in testing Stripe without real cards.
The practical split we recommend: mocks for edge-case status codes you want to simulate on demand, clones for any flow where state accumulates — checkout, subscription lifecycle, OTP delivery, file upload then download. For SMS and email verification flows, clones pair naturally with inbox capture; see Twilio SMS/OTP testing and email workflow testing.
04World snapshot / restore
Once your dependencies hold state, tests start interfering with each other: one run’s refund becomes another run’s unexpected balance. The fix borrowed from database testing is snapshot and restore — capture the entire simulated world before a scenario, restore it after, or fork it per worker:
# Fork an isolated world per parallel worker,
# seeded from a known-good baseline
world := clones.Fork("checkout-baseline@v14")
defer world.Discard()
stripe := world.Stripe()
cust, _ := stripe.Customers.Create(...)
charge, _ := stripe.Charges.Create(cust.ID, 4200, "usd")
// assert, mutate, break things — nothing leaks
refund, _ := stripe.Refunds.Create(charge.ID)
if cust.Balance != -4200 { t.Fatal("balance not updated") }
Two properties matter more than the mechanism. First, restore must be cheap — sub-second — or
teams will batch scenarios and lose isolation anyway. Second, snapshots should be named and versioned
like fixtures, so a test failure can say “reproducible against baseline@v14” instead of
“works on my machine.”
05Webhooks: signature parity or it didn’t happen
Most third-party integrations have two halves: the API you call, and the webhooks that call you. Mocks usually skip the second half entirely, which is why webhook handlers are chronically untested. A clone that emits events must also sign them exactly as the vendor does — same header format, same timestamp tolerance, same HMAC scheme — so your verification code runs for real:
// Your existing handler, unchanged, verifying a cloned event
const event = stripe.webhooks.constructEvent(
rawBody, // exact bytes the clone sent
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === 'charge.refunded') { ... }
Test the unhappy paths too: replayed events (same ID, second delivery), stale timestamps outside the tolerance window, and signatures computed with the wrong secret. Those three cases cause most real webhook incidents, and none of them are reachable through a sandbox UI.
06A rollout checklist
- Inventory every third-party call in critical flows: payments, email, SMS, storage, auth.
- Classify: read-only (safe-ish), stateful (needs clones), side-effecting (never live).
- Swap stateful dependencies to clones via env-based base URLs — no code changes.
- Add the live-key refusal assertion to CI startup.
- Seed named world snapshots for each critical flow; version them with the app.
- Prove webhook handling with signed cloned events, including replays and bad signatures.
Molar ships destruction-safe clones of Stripe, Twilio, email, S3, auth, and GitHub behind this exact model — deterministic IDs, virtual clock for expiry-sensitive logic, snapshot/restore per run — and Trace records every cloned interaction as evidence next to DOM and network captures. If you want the deeper walkthrough for one vendor, start with Stripe without real cards.
Run your scariest tests with zero blast radius
Molar gives your suite stateful clones of Stripe, S3, email, SMS and auth — real semantics, deterministic IDs, nothing destructive ever leaves the building.