Mocking and Stubbing APIs for Reliable Tests
A test suite that calls a real third-party payment gateway, a flaky internal service, or an API that allows only 100 requests a day is not a test suite — it's a liability. Mocking and stubbing let you test your own code against a dependency without depending on that dependency being up, fast, or free. Done well, this makes suites faster and deterministic. Done badly, it makes them lie about what works in production. Here's how to tell the difference.
1. Stub, mock and fake are not the same thing
The terms get used interchangeably, but the distinction matters when you're deciding what to build:
- Stub — returns a canned response, no questions asked. You use it to get a dependency out of the way so you can test the code around it. "When this endpoint is called, return this JSON."
- Mock — a stub with expectations. It can assert that it was called, with what arguments, how many times. You use it to verify your code talks to the dependency correctly, not just that it survives the response.
- Fake — a lightweight working implementation (an in-memory database instead of Postgres, a mock server that actually tracks state across calls). More realistic than a stub, cheaper than the real thing.
Most API test suites only need stubs: fixed responses for fixed scenarios. Reach for mocks when you need to verify a call happened (did we actually send the webhook retry?); reach for fakes when a single stubbed response can't represent a multi-step flow (create → fetch → update).
2. Decide what you're actually testing before you mock anything
Mocking removes the real dependency from the test — that's the point, but it also means the test can't catch anything that depends on the dependency behaving as assumed. Use this split to decide when mocking is the right call:
| You're testing... | Mock the dependency? |
|---|---|
| Your error handling when a payment provider times out | Yes — you can't reliably force a real timeout |
| Your retry logic on a 503 | Yes — forcing a real 503 on demand isn't practical |
| Whether your integration with the provider actually works end to end | No — mocking this just tests your assumptions about the provider, not the provider |
| A third-party API with a hard rate limit or per-call cost | Yes for most runs, with a small smoke suite against the real thing |
| An internal service your team owns and can run locally | Prefer running it for real in CI; mock only for unit-level isolation |
The rule of thumb: mock dependencies you don't control or can't reliably exercise; keep at least a small number of tests hitting the real thing so mocked assumptions get checked against reality on a schedule, not never.
3. Three places to put the mock
- In-process stub. Replace the HTTP client or SDK call directly in the test (e.g.
nockin Node,responsesin Python,WireMock's Java API). Fast, no network, but only covers code that goes through that client. - Local mock HTTP server. A real server (WireMock, Prism, json-server, or a few lines of Express/Flask) that listens on a port and returns canned responses to real HTTP requests. Point your app's
baseUrlat it via an environment variable. This is the better fit for black-box API testing, because you're exercising the real network call, serialization and headers — just against a fake backend. - Shared service virtualization. A hosted mock (WireMock Cloud, a staging double maintained by the team that owns the real API) that multiple teams point to. Worth it once several consumers need the same dependency mocked consistently.
For API client testing specifically, the local mock server is usually the sweet spot: define a RestRuno environment with baseUrl = http://localhost:4010 for "mocked" and baseUrl = https://api.example.com for "live," and run the exact same collection against either — no changes to the requests themselves.
4. A minimal mock server, two ways
If the real API has an OpenAPI spec, generate the mock from it instead of hand-writing responses — this is the single biggest thing you can do to stop mocks drifting from reality:
# Prism mocks straight from an OpenAPI document
npx @stoplight/prism-cli mock openapi.yaml --port 4010
Without a spec, a few explicit stubs are often enough:
// WireMock-style stub mapping
{
"request": { "method": "GET", "urlPath": "/v1/orders/42" },
"response": {
"status": 200,
"jsonBody": { "id": 42, "status": "shipped", "total": 58.90 },
"headers": { "Content-Type": "application/json" }
}
}
// Also stub the failure path you can't trigger on demand
{
"request": { "method": "GET", "urlPath": "/v1/orders/999" },
"response": { "status": 503, "jsonBody": { "error": "upstream_unavailable" } }
}
5. Keep mocks from lying
A stale mock is worse than no mock — it reports green while production is red. Guard against drift:
- Generate from the contract when one exists. An OpenAPI-driven mock breaks the moment the spec changes, which is exactly when you want to know. See automating API tests in CI/CD for wiring a schema-validation step alongside it.
- Run a thin contract check against the real API on a schedule (nightly, not per-commit) so a provider's breaking change surfaces within a day instead of at the next incident.
- Mock at the boundary, not deep inside your code. Stub the HTTP layer, not your own business logic — otherwise you're testing the mock, not your integration with it.
- Version mock fixtures with the code that depends on them. A fixture that changed without a corresponding code change is a signal, not noise.
- Label mocked test runs clearly in CI output so a green "mocked" suite is never mistaken for a verified integration.
Quick checklist
- ✔ Picked stub, mock or fake based on what you need to verify — not habit
- ✔ Mocked only dependencies you don't control or can't reliably trigger failures in
- ✔ Kept a small suite of tests hitting the real dependency
- ✔ Mock server generated from an OpenAPI spec where one exists
- ✔ Explicit stubs for failure paths (timeouts, 5xx, rate limits)
- ✔ Same collection runs against mocked and live environments via one variable
- ✔ Mocked runs clearly labeled in CI so they're never confused with real integration coverage
Switch between mocked and live in one click
RestRuno's environments let you point the same collection at a local mock server or the real API by switching a variable — no edits to requests. Free, local-first, and your collections stay as plain JSON files you can commit.
Download RestRuno — free