API Mocking for Testing
Build reliable tests by mocking external APIs with WireMock, MockServer, and MSW to eliminate flakiness and test edge cases.
Overview
API mocking swaps real external dependencies for controlled simulations during testing. Your suite stops depending on the network: no flakes, faster runs, and edge cases like 500s or timeouts become trivial to reproduce. Each tool fakes the boundary differently — WireMock runs as a real HTTP server, MSW intercepts requests in-process, and responses patches Python’s HTTP layer — so you pick whichever sits closest to your stack.
When to Use
Use this resource when:
- External APIs are unreliable, slow, or have rate limits that block CI pipelines
- You need to test error handling for HTTP 429, 503, or timeout scenarios
- The real service doesn’t have a sandbox or test environment
- You want deterministic tests that don’t fail due to third-party changes
Skip API mocking when:
- You’re testing your own database or internal logic — use an in-memory engine or Testcontainers and run the real thing
- You need proof the provider honors its API — that’s contract testing, not mocking
Solution
WireMock Standalone (Java)
import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
public class PaymentApiMock {
private static WireMockServer wireMockServer;
public static void start() {
wireMockServer = new WireMockServer(8089);
wireMockServer.start();
wireMockServer.stubFor(
post(urlEqualTo("/payments"))
.withHeader("Content-Type", equalTo("application/json"))
.withRequestBody(matchingJsonPath("$.amount"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\": \"pay_123\", \"status\": \"succeeded\"}")
)
);
// Error scenario
wireMockServer.stubFor(
post(urlEqualTo("/payments"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("999999")))
.willReturn(aResponse()
.withStatus(400)
.withBody("{\"error\": \"amount_exceeds_limit\"}")
)
);
}
public static void stop() {
wireMockServer.stop();
}
}
MSW (Mock Service Worker) for Browser/Node
// MSW 2.x — the `rest` API from v1 was removed
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const handlers = [
http.get('https://api.example.com/users/:id', ({ params }) => {
const { id } = params;
return HttpResponse.json({ id, name: 'Test User', email: 'test@example.com' });
}),
http.post('https://api.example.com/orders', async ({ request }) => {
const { total } = await request.json();
return HttpResponse.json({ orderId: 'ord_456', total }, { status: 201 });
}),
// Error simulation
http.get('https://api.example.com/flaky', () => {
return HttpResponse.json({ error: 'Service Unavailable' }, { status: 503 });
})
];
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Python responses Library
import responses
import requests
@responses.activate
def test_payment_api():
responses.add(
responses.POST,
'https://payments.example.com/charge',
json={'id': 'ch_123', 'status': 'succeeded'},
status=200
)
result = requests.post(
'https://payments.example.com/charge',
json={'amount': 100, 'currency': 'USD'}
)
assert result.json()['status'] == 'succeeded'
assert len(responses.calls) == 1
assert responses.calls[0].request.json()['amount'] == 100
Simulating Latency and Failures
Fast mocks that always return 200 hide the bugs that matter. Make them slow and unreliable on purpose:
// MSW 2.x: latency and network-level failures
import { http, HttpResponse, delay } from 'msw';
http.get('https://api.example.com/slow-report', async () => {
await delay(5000); // trips any client timeout under 5s
return HttpResponse.json({ ok: true });
}),
http.get('https://api.example.com/unreachable', () => {
return HttpResponse.error(); // hard network failure, not an HTTP status
})
HttpResponse.error() fires a connection-level failure — a reset or DNS error, the kind a 503 never reproduces. Add delay() calls to prove the client honors its own timeout budget; a request that never resolves hurts more than a 500.
Stateful Mocks
Real APIs change state between calls — a job goes pending → running → done. WireMock models this with scenarios, where a stub can transition the scenario after it matches:
// First poll returns 202, the next one 200 — test polling without real sleeps
wireMockServer.stubFor(get(urlEqualTo("/jobs/42"))
.inScenario("job-lifecycle")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(aResponse().withStatus(202).withBody("{\"state\": \"running\"}"))
.willSetStateTo("done"));
wireMockServer.stubFor(get(urlEqualTo("/jobs/42"))
.inScenario("job-lifecycle")
.whenScenarioStateIs("done")
.willReturn(aResponse().withStatus(200).withBody("{\"state\": \"done\"}")));
Scenarios let one endpoint answer differently in sequence — the only sane way to test polling, retry-then-recover logic, and multi-step workflows.
Explanation
Three mocking strategies:
| Strategy | Level | Best For |
|---|---|---|
| HTTP server proxy | Network | Integration tests; verify real HTTP clients |
| Request interceptor | Application | Unit tests; browser/Node unified mocking |
| Service virtualization | System | Complex stateful APIs; contract testing |
Request matching hierarchy:
- Exact URL —
GET /users/123 - Path pattern —
GET /users/* - Header match —
Content-Type: application/json - Body match — JSON path or regex on request body
- State-dependent — Return different response on second call
Mock vs. stub vs. fake vs. contract test — these get conflated constantly:
| Technique | Runs real code? | Records calls? | Verifies the provider? |
|---|---|---|---|
| Stub | No | No | No |
| Mock | No | Yes | No |
| Fake | Yes, simplified | No | No |
| Contract test | Provider’s real code | — | Yes |
Pick by the question you’re answering: “does my code handle response X?” → stub or mock. “Did my code call the API correctly?” → mock. “Do my queries work?” → fake or a real database. “Does the provider still honor its contract?” → contract test.
Variants
| Tool | Language | Best Feature |
|---|---|---|
| WireMock | Java/Any | Stateful scenarios; proxy recording |
| MSW | TypeScript | Same mocks in browser, Node, and tests |
| MockServer | Any | JSON expectation API; verification |
| responses | Python | Decorator-based; simple assertions |
| Nock | Node.js | Chained API; recorder mode |
What Works
- Mock at the boundary: Mock HTTP, not internal methods — tests should exercise the full stack.
- Verify requests, not just responses: Ensure your code sends the right payload and headers
- Use record/replay for complex APIs: Capture real traffic once, then replay in tests
- Keep mocks close to reality: Update mocks when the real API changes; stale mocks hide bugs
- Reset between tests: Clean state to prevent one test’s setup from affecting another
Common Mistakes
- Mocking internal methods: You test the mock, not the code
- Overly permissive matchers:
any()matchers let bugs through that specific matchers catch - No error scenario coverage: Only testing 200 OK misses half your error handling code
- Shared mutable state: Global mock state leaks between tests
- Forgetting to verify: A passing test with an unused mock means nothing was actually tested
Further Reading
- WireMock documentation — stubbing, scenarios, and record-and-play
- MSW documentation — request interception for browser and Node
- MockServer documentation — expectation API and request verification
- responses on GitHub — the Python library used above
Frequently Asked Questions
Should I mock my own service's database?
No. Reach for mocks on boundaries you don't own — a third-party API, a payment gateway, an email provider. Your database is a different story: boot it for real with Testcontainers or an in-memory engine. A mocked DB tells you the mock works; it says nothing about your queries.
What's the difference between mocking and stubbing?
Stubs return canned answers and forget the call happened. Mocks record every call, so you can assert on them — "did my code hit this endpoint with these args?" Use stubs for data, and mocks when the interaction itself is the thing under test.
Can mocks replace contract testing?
No. A mock answers "if the API responds like X, does my code do Y?" — it never asks whether the provider actually returns X. That second question is what contract tests exist for. You want both: mocks keep the suite fast, contracts keep it honest.
How do I keep mocks in sync with the real API?
Derive them from the provider's OpenAPI spec or from recorded traffic — WireMock's record-and-play, VCR-style cassettes — and re-record on a schedule. A stale mock keeps passing tests against an API that no longer exists; that drift is the real price of mocking.
Should mock servers live in my test suite or a shared environment?
In the suite, spun up per test — shared mock servers turn into shared mutable state and flaky CI. WireMock, MSW, and responses all run in-process or per-test; reserve a shared env only for exploratory QA work.
Related Resources
CI/CD Pipeline Guide: From Commit to Production
A practical guide to building CI/CD pipelines with GitHub Actions, testing gates, deployment strategies, and rollback procedures.
GuideSoftware Testing Strategy Guide
A practical guide to building a layered testing strategy with unit, integration, contract, and end-to-end tests that your team will actually maintain.
GuideTest-Driven Development (TDD) — A Practical Workflow
Learn TDD step by step: write a failing test, make it pass, refactor. Red-Green-Refactor with real examples in Python, JavaScript, and Java.
RecipeStub External HTTP APIs with WireMock in Java
Use WireMock in Java tests to stub external HTTP services. Covers response templating, delay simulation, stateful stubs, and request verification.
RecipeMock GraphQL Resolvers for Frontend Development
Set up mocked GraphQL resolvers with Apollo Server so frontend teams can develop against a fake API before the backend is ready.