API Mocking vs Stubbing: Mocks, Stubs, Fakes and Spies
Quick answer
A stub returns canned responses so the code under test can run; a mock also verifies that the expected calls were made, and fails the test if they were not. Fakes are working lightweight implementations, spies record calls for later assertions, and service virtualization simulates whole dependencies, often with state. Pick the lightest double that answers your test's question.
Reviewed by Parveen Kumari
Part of the Test Data & API Mocking hub. Start with the pillar guide: Test data management best practices.
Mocking vs stubbing comes down to one question: does the test verify the calls, or only the results? A stub stands in for an API and returns canned responses so the code under test can run. A mock does the same and also checks that the expected requests were made, failing the test if they were not. Stubs support state-based tests; mocks support behaviour-based tests.
Around those two sit three more doubles you will meet in API work: fakes, spies and service virtualization. This guide explains each, shows when to use which, and builds examples with WireMock, MSW and Prism. It belongs to our test data management hub; for the team-level reasons to mock at all, read API mocking for parallel development first.
The five API test doubles, defined
The vocabulary comes from Gerard Meszaros' xUnit Test Patterns, popularised by Martin Fowler's essay Mocks Aren't Stubs. Translated to HTTP dependencies:
| Double | What it does at the API layer | Question it answers | Fails the test when |
|---|---|---|---|
| Stub | Returns a fixed response for a matching request | "Does my code handle this response correctly?" | Never on its own; your assertions on the output fail |
| Spy | A stub that also records every request it receives | "What did my code send?" (checked after the fact) | Your assertions on the recorded calls fail |
| Mock | A stub with expectations set up front and verified | "Did my code make exactly these calls?" | An expected call is missing, extra or malformed |
| Fake | A working, simplified implementation (in-memory store, local emulator) | "Does a multi-step flow work against realistic behaviour?" | Your assertions fail; the fake itself behaves like the real thing |
| Service virtualization | A shared, long-lived simulation of a whole service, often stateful, with recorded traffic, latency and faults | "Can teams and environments work without the real dependency?" | Depends on the tests that use it |
Two clarifications save a lot of argument. First, most tools called "mock servers" are stub servers: they return responses but nobody checks the calls. That is fine; just know which one you are using. Second, a dummy, the fifth Meszaros double, is an object passed only to fill a parameter list. It rarely matters at the HTTP level.
Stubbing an API: canned responses
Stub when the test's real subject is your code's handling of a response: parsing, mapping, error handling, retries. You care what comes back, not how often it was asked for.
A WireMock stub mapping, served from wiremock/mappings/rates.json:
{
"request": {
"method": "GET",
"urlPath": "/rates",
"queryParameters": { "currency": { "equalTo": "EUR" } }
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": { "currency": "EUR", "rate": 1.08 }
}
}
docker run --rm -p 8080:8080 -v "$PWD/wiremock:/home/wiremock" wiremock/wiremock
curl "http://localhost:8080/rates?currency=EUR"
# {"currency":"EUR","rate":1.08}
The real value of stubs is in the responses the real API rarely gives you on demand. Add mappings for a 429 with Retry-After, a 503, a slow response ("fixedDelayMilliseconds": 3000) or a dropped connection ("fault": "CONNECTION_RESET_BY_PEER") and you can test timeout and retry logic deterministically.
For spec-first teams, Prism stubs a whole API from its OpenAPI document with no mappings at all:
npx @stoplight/prism-cli mock openapi.yaml # static: returns spec examples
npx @stoplight/prism-cli mock -d openapi.yaml # dynamic: generates schema-valid data
curl -H "Prefer: code=404" http://127.0.0.1:4010/orders/42
The Prefer header picks a documented response code or named example, so a test can ask for the error path without a separate mapping.
Hand-written stub mappings drift the moment the spec changes. If you would rather generate stubs for every endpoint straight from your OpenAPI file, start a free trial and import your spec.
Generate test data variants from your spec (free)
Free, runs in your browser, no sign-up needed.
Mocking an API: verifying the calls
Mock when the call itself is the behaviour under test: a payment must be captured exactly once, an audit event must carry the user ID, a webhook must not fire for a dry run. Here the response is incidental; the request is the assertion.
With WireMock's Java DSL, you stub and then verify:
import static com.github.tomakehurst.wiremock.client.WireMock.*;
stubFor(post(urlEqualTo("/payments"))
.willReturn(aResponse().withStatus(201)));
checkoutService.placeOrder(order); // code under test
verify(1, postRequestedFor(urlEqualTo("/payments"))
.withHeader("Idempotency-Key", matching("[0-9a-f-]{36}"))
.withRequestBody(matchingJsonPath("$.currency", equalTo("EUR"))));
The same check is available over HTTP from any language through the admin API:
curl -s -X POST http://localhost:8080/__admin/requests/count \
-d '{"method":"POST","url":"/payments"}'
# {"count":1}
Use mocks sparingly. Every expectation couples the test to an implementation detail, so a refactor that changes call order or batches two calls into one breaks tests even though the behaviour is still correct. Verify the calls that are part of the contract (charged once, idempotency key present) and nothing more.
Spies: record now, assert later
A spy sits between stub and mock: it answers like a stub and keeps a log you inspect after the code has run. That keeps the arrange-act-assert order intact and avoids up-front expectations.
MSW (Mock Service Worker) intercepts fetch and HTTP calls in Node.js and the browser, so it suits JavaScript clients. Version 2 syntax with a request log:
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const calls = [];
const server = setupServer(
http.get('https://api.example.com/users/:id', ({ params }) =>
HttpResponse.json({ id: params.id, name: 'Ada' })
)
);
server.events.on('request:start', ({ request }) => {
calls.push({ method: request.method, url: request.url });
});
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => {
server.resetHandlers();
calls.length = 0;
});
afterAll(() => server.close());
test('profile page loads the user once', async () => {
const user = await loadProfile('42'); // code under test
expect(user.name).toBe('Ada'); // state check (stub)
expect(calls).toHaveLength(1); // behaviour check (spy)
});
onUnhandledRequest: 'error' is worth copying everywhere: any request your handlers do not cover fails the test instead of leaking to the real network.
Fakes and service virtualization
Fakes earn their cost when a test spans several calls with state between them: create an order, add a line, read it back. Stubbing that needs a canned response per step and breaks on every change. A fake, such as an in-memory repository swapped in through dependency injection, a local emulator like LocalStack or Azurite, or a Testcontainers instance of the real database, behaves like the real thing for the paths you care about. WireMock can approximate light statefulness with scenarios, but past a few states a real fake is simpler.
Service virtualization moves the double out of the test and into the environment. A virtual service is shared, long-lived and usually built from recorded traffic, with latency and fault profiles. Teams use it when a dependency is expensive per call, rate limited, owned by a partner, or not built yet, and many pipelines need it at once. WireMock's record-and-playback mode, Hoverfly and Mountebank cover the open-source end; our API mocking tools roundup compares them.
Free 1-page checklist
API Testing Checklist for CI/CD Pipelines
A printable 25-point checklist covering authentication, error scenarios, contract validation, performance thresholds, and more.
Download FreeThe trade-off is ownership. A virtual service is a product someone must keep current; left alone, it becomes a museum of last year's API.
When to use which: a decision guide
| Your test needs to... | Use | Typical tool |
|---|---|---|
| Exercise response handling, errors, timeouts | Stub | WireMock, Prism, MSW |
| Develop a client before the provider exists | Stub generated from the spec | Prism, spec-driven mock server |
| Prove a call was made exactly once, with the right body | Mock | WireMock verify, nock scope.isDone() |
| Check what was sent without up-front expectations | Spy | MSW events, WireMock request journal |
| Run multi-step flows with state | Fake | In-memory implementation, Testcontainers |
| Give many teams a stand-in for an expensive or unavailable system | Service virtualization | WireMock record/playback, Hoverfly, Mountebank |
| Prove your doubles still match the real API | None: contract test | OpenAPI validation, Pact |
A useful default for API work: stub by default, mock only the calls that are part of the contract, and add a fake when stubs start multiplying.
The drift problem: keep doubles honest
Every double encodes an assumption about the real API. When the provider renames a field, your stubs keep returning the old one and your tests keep passing. That is how mocked suites go green while production breaks.
Three habits prevent it:
- Generate doubles from the contract. Stubs built from the OpenAPI spec (Prism, or a spec-driven mock server) change when the spec changes, instead of when someone remembers.
- Validate the provider against the same contract. A schema-first contract test checks the real API against the spec your stubs came from, so drift fails a build on one side or the other.
- Run a thin layer against the real dependency. A handful of smoke tests against a sandbox catch what no contract describes, such as auth quirks or rate limits.
The same rule applies to our own tool. The Shift-Left API mock server generates mocks for up to 500 endpoints at once from imported OpenAPI, WSDL or GraphQL definitions, with conditional and templated responses and per-response delays. In this article's terms it is a stub server: it is not stateful, it does not verify calls, and tests reach it only when an environment's base URL points at the mock. Pair it with contract tests and keep mock-style verification in code, as in the WireMock and MSW examples above.
API mocking vs stubbing FAQ
What is the difference between mocking and stubbing an API?
A stub replaces an API with canned responses so the code under test can run; the test asserts on the code's own output. A mock is a stub with expectations: the test also verifies that particular requests were made, and fails if they were not. Stubs check state, mocks check behaviour.
Is a mock server a mock or a stub?
Usually a stub. A "mock server" that returns responses from a spec or recorded examples is stubbing. It becomes mocking only when the test verifies the interactions, for example with WireMock's verify API.
What is the difference between service virtualization and mocking?
Mocking is test-scoped: set up for one test and thrown away. Service virtualization simulates a whole dependency, often with state, recorded traffic and fault profiles, as a shared, long-lived service many teams use.
When should I use a fake instead of a mock?
When the test needs realistic behaviour across several calls, such as create then read, and a stub would need too many canned responses. In-memory repositories, local emulators and Testcontainers are fakes.
Can mocks replace contract testing?
No. Doubles encode assumptions that can drift from the real API. Contract testing checks the provider against the same contract the doubles were built from, which closes the gap.
Sources and further reading
Ready to shift left with your API testing?
Import your OpenAPI spec and get a runnable test suite, in our cloud or on your own infrastructure.