How to mock Stripe, Auth0 and other external APIs in integration tests
Almost every application depends on APIs it does not control. Stripe takes the payment, Auth0 issues the token, Twilio sends the message, some internal platform team owns the service two hops away.
Testing against those dependencies is where most integration test suites quietly fall apart.
The four problems with testing against a sandbox
The obvious approach is to point your test environment at the provider's sandbox. It works, until it doesn't.
Rate limits. Sandboxes are throttled. Run your suite on every pull request across a few engineers and you will hit the ceiling. Your tests start failing for reasons that have nothing to do with your code.
Shared state. Test accounts accumulate data. A customer created by last week's run is still there. Two engineers running the suite at the same time step on each other. Tests pass locally and fail in CI because the account was in a different state.
Sandbox is not production. Behaviour differs in small ways that matter. Response timing, rare fields, specific error codes, webhook ordering. You are testing against an approximation and calling it real.
You cannot force failures. This is the big one. What does your checkout do when Stripe returns a 500? When a card is declined for insufficient funds specifically? When Auth0 times out mid-request? You cannot reliably trigger any of those in a sandbox. So the error paths, which are exactly the paths that break in production, never get tested.
That last point is worth sitting with. Most teams have thorough tests for the happy path and nothing at all for the failure modes.
Why unit-level mocking is not enough
The usual answer is to mock at the code level. Stub the Stripe SDK in Jest, return a fake response, assert your handler does the right thing.
That tests your handler. It does not test:
- Whether your service actually constructs the request correctly
- Whether the HTTP layer, retries and timeouts behave as expected
- Whether the response gets parsed and passed downstream correctly
- What the service two hops away does with the result
You have mocked away the integration, in an integration test. The seam you most wanted to check is the seam you replaced with a stub.
Mocking at the network layer instead
The alternative is to intercept the call where it actually happens: on the wire, between your service and the outside world.
Your service makes a real HTTP request. Your code path runs unchanged, including the client library, retries and error handling. The request just never leaves the test environment. A mock answers it with whatever you specify.
That gives you three things a code-level stub cannot:
Your service is genuinely unmodified. No test-only branches, no injected fakes, no environment flags that behave differently in production.
You can assert on what was sent. Not just what your function returned, but the actual outbound request: the headers, the payload, the idempotency key, whether it was retried.
You control the response completely. Any status code, any body, any delay. The failure paths become as easy to test as the happy path.
The hard case: one endpoint, many behaviours
Simple mocking maps a URL to a response. That falls down fast in practice.
Think about an LLM provider. Every call goes to the same endpoint, and the only difference is the prompt in the request body. Or a GraphQL API, where every query is a POST to /graphql. Or an RPC-style service where the method name is a field, not a path.
A URL-based mock cannot distinguish between these. You need routing based on what is inside the request.
Dokkimi supports body-aware mock routing for exactly this: return different responses from the same endpoint depending on the content of the request body. That is what makes it possible to test LLM prompt routing, GraphQL resolvers, and RPC APIs where one URL serves everything.
What this looks like in a test
You declare the mock as part of the environment, alongside your services:
name: checkout-with-declined-card
items:
- $ref: ../shared/web-app.yaml
- $ref: ../shared/order-service.yaml
- $ref: ../shared/postgres-db.yaml
- $ref: ../shared/mock-stripe.yaml
tests:
- name: Declined card leaves the order unpaid
steps:
- action:
type: ui
url: http://web-app:3000/checkout
subSteps:
- action: click
selector: '[data-testid="pay"]'
- action: waitForSelector
selector: '[data-testid="payment-failed"]'
- action: screenshot
name: payment-declined
assertions:
# Assert the outbound call to Stripe was correct
- match:
path: $.traffic
where:
- path: $$.origin
operator: eq
value: order-service
- path: $$.request.url
operator: contains
value: /v1/payment_intents
count: 1
assertions:
- path: $.match.request.method
operator: eq
value: POST
# Assert the order was not marked paid
- action:
type: dbQuery
database: postgres-db
query: "SELECT status FROM orders ORDER BY created_at DESC LIMIT 1"
assertions:
- path: $.response.data[0].status
operator: eq
value: 'payment_failed'The mock returns a declined card response. The service handles it. The UI shows the failure state. The database records the order as unpaid, not paid.
That is a test of a failure path, running on every pull request, with no Stripe account involved.
Auth is the other one worth mocking properly
Authentication is the dependency that makes most people give up on integration testing.
Your services validate a JWT against a JWKS endpoint. In a test environment you either point at the real Auth0 tenant, which means network calls and shared state, or you disable auth entirely, which means you are testing a system that does not resemble production.
The third option is to mock the JWKS endpoint. Your services keep validating tokens exactly as they do in production. The keys just come from a mock you control. You can issue valid tokens, expired tokens, tokens with the wrong audience, tokens signed with the wrong key, and assert on how each one is handled.
The demo repo at github.com/dokkimi/demo-nextjs does this with Google OAuth: a full Next.js app with Postgres, real auth flow, mocked identity provider, running in CI.
The error cases worth writing first
If you are adding failure-path tests, these tend to be the highest value:
- Payment provider returns a 500 mid-transaction
- Card declined for insufficient funds, as distinct from a generic decline
- Auth provider times out during token validation
- Rate limit response from a third party under load
- Malformed or unexpected response body
- The same request arriving twice, testing your idempotency handling
Every one of those happens in production. None of them are reliably reproducible against a sandbox.
Getting started
brew install dokkimi/tap/dokkimi
# or
npm install -g dokkimi
dokkimi init # scaffolds a .dokkimi/ folder with examples
dokkimi doctor # verify Docker and Node are set up
dokkimi runRequires Node 20+ and Docker. Free locally and in CI.
Mocks are declared as YAML fragments and referenced with $ref, so one mock-stripe.yaml is shared across every test that needs it. When the provider changes, you update one file.
If you use Claude Code, Cursor or GitHub Copilot, Dokkimi's MCP server auto-registers on install. You can ask your agent to write a mock definition for a specific failure case and it has the full spec available as native tool calls.
Docs at dokkimi.com/docs.