Skip to content
SP StackPractices
intermediate By StackPractices

API Mocking for Testing

Build reliable tests by mocking external APIs with WireMock, MockServer, and MSW to eliminate flakiness and test edge cases.

Topics: testing

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

API mocking replaces real external dependencies with controlled simulations during testing. This eliminates network flakiness, reduces test execution time, and enables testing edge cases — like 500 errors or timeouts — that are hard to reproduce with live services. Modern tools like WireMock, MSW, and MockServer provide request matching, response templating, and verification capabilities that make mocks behave like the real thing.

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

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

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const handlers = [
  rest.get('https://api.example.com/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    return res(
      ctx.status(200),
      ctx.json({ id, name: 'Test User', email: 'test@example.com' })
    );
  }),

  rest.post('https://api.example.com/orders', (req, res, ctx) => {
    return res(
      ctx.status(201),
      ctx.json({ orderId: 'ord_456', total: req.body.total })
    );
  }),

  // Network error simulation
  rest.get('https://api.example.com/flaky', (req, res, ctx) => {
    return res(ctx.status(503), ctx.json({ error: 'Service Unavailable' }));
  })
];

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

Explanation

Three mocking strategies:

StrategyLevelBest For
HTTP server proxyNetworkIntegration tests; verify real HTTP clients
Request interceptorApplicationUnit tests; browser/Node unified mocking
Service virtualizationSystemComplex stateful APIs; contract testing

Request matching hierarchy:

  1. Exact URLGET /users/123
  2. Path patternGET /users/*
  3. Header matchContent-Type: application/json
  4. Body match — JSON path or regex on request body
  5. State-dependent — Return different response on second call

Variants

ToolLanguageBest Feature
WireMockJava/AnyStateful scenarios; proxy recording
MSWTypeScriptSame mocks in browser, Node, and tests
MockServerAnyJSON expectation API; verification
responsesPythonDecorator-based; simple assertions
NockNode.jsChained API; recorder mode

Best Practices

  • Mock at the boundary: Mock HTTP, not internal methods — tests should exercise the full stack. For full integration coverage, see end-to-end testing.
  • 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

  1. Mocking internal methods: You test the mock, not the code
  2. Overly permissive matchers: any() matchers let bugs through that specific matchers catch
  3. No error scenario coverage: Only testing 200 OK misses half your error handling code
  4. Shared mutable state: Global mock state leaks between tests
  5. Forgetting to verify: A passing test with an unused mock means nothing was actually tested

Frequently Asked Questions

Q: Should I mock my own service’s database? A: No. Use an in-memory database or TestContainers. Mock external APIs, not your own dependencies.

Q: What’s the difference between mocking and stubbing? A: Stubs return canned responses. Mocks also verify interactions (was this method called with these args?).

Q: Can mocks replace contract testing? A: No. Mocks test your assumptions about the API. Contract testing verifies both sides agree on the schema.