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:

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:

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 run

Requires 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.