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 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:
| 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
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
Error Handling in Tests
- Test failure handling: handle test failures with clear assertions. Capture screenshots on UI test failures. Log test environment details on failure.
- Test timeout handling: set appropriate timeouts for each test. Unit tests should complete in seconds. Integration tests may need longer timeouts. E2E tests need generous timeouts.
- Flaky test management: identify and fix flaky tests. Quarantine flaky tests. Fix root cause of flakiness.
Security in Testing
- Test data security: use synthetic data for testing. Never use real production data in tests. Mask sensitive fields in test data. Encrypt test databases.
- Test environment security: secure test environments. Restrict access to test environments.
- Secrets in tests: never hardcode secrets in test files. Use test-specific secret management. Rotate test secrets regularly.
Deployment and CI/CD for Tests
- Test pipeline design: design CI/CD pipeline for tests. Run unit tests on every commit. Run integration tests on pull requests. Run E2E tests before deployment. Run security scans on every build.
- Test parallelization: parallelize tests for faster execution. Group tests by dependency. Isolate parallel tests.
- Test result reporting: report test results clearly. Publish reports to stakeholders.
Testing Tools and Platforms
- Unit testing frameworks: choose the right unit testing framework. Jest for JavaScript. JUnit 5 for Java. Vitest for modern JavaScript.
- Integration testing tools: use appropriate tools for integration testing. TestContainers for Docker-based integration tests. Supertest for API testing. WireMock for external service mocking. MSW for browser API mocking.
- E2E testing tools: choose the right E2E testing tool. Playwright for modern web E2E. Cypress for web applications. Selenium for legacy web apps. Detox for React Native.
Common Testing Pitfalls
- Over-mocking: avoid mocking too much. Mock only external dependencies. Mock only what you need to control. Excessive mocking makes tests brittle. Refactor over-mocked tests.
- Testing implementation details: test behavior, not implementation. Avoid testing internal state. Focus on public API behavior. Refactor implementation-coupled tests. Educate team on behavior testing.
- Ignoring edge cases: test edge cases thoroughly. Test null values. Test error paths.
Best Practices
- Test naming conventions: use descriptive test names. Follow arrange-act-assert pattern. Name tests by behavior, not implementation. Educate team on conventions. Refactor poorly named tests.
- Test organization: organize tests by feature or component. Group related tests in describe blocks. Refactor large test files.
- Test data management: use factories for test data. Use fixtures for static data. Refactor duplicated test data.
- Test coverage goals: set realistic coverage goals. 80% for critical paths. 60% for utility code. 100% for pure functions.
Cost Optimization
- Reducing test execution time: optimize test execution speed. Cache test dependencies. Refactor slow tests.
- Reducing test maintenance: minimize test maintenance overhead. Write maintainable tests. Refactor duplicated test code.
- Test infrastructure costs: optimize test infrastructure costs. Use containerized test environments. Scale test infrastructure with demand.
Troubleshooting Guide
- Debugging failing tests: isolate the failing test. Run the test in isolation. Verify test environment. Use root cause analysis.
- Debugging slow tests: identify slow tests. Profile test execution. Check network calls.
- Debugging test environment issues: check environment configuration. Verify dependencies are installed. Verify database state.
Monitoring and Alerting
- Key test metrics: track test pass rate, execution time, coverage, and flaky test rate. Adjust thresholds based on trends.
- Alert configuration: set alerts on test failure rate above 5%. Alert on flaky test rate increases. Reduce alert noise.
- Test reporting dashboards: create dashboards for test metrics. Show pass rate, coverage, and trends.
Advanced Testing Patterns
- Property-based testing: use property-based testing for edge case discovery. Define properties that should always hold. Let the framework generate test cases. Run many iterations to find counterexamples.
- Mutation testing: use mutation testing to evaluate test quality. Mutate source code and run tests. Good tests catch mutations. Calculate mutation score.
- Snapshot testing: use snapshot testing for regression detection. Capture component output as snapshot.
Migration Strategies
- Migrating from manual to automated testing: start with critical paths. Add integration tests next. Add unit tests for new code. Gradually add tests for legacy code.
- Migrating between test frameworks: plan framework migration carefully. Map old assertions to new framework. Migrate tests incrementally. Run both frameworks in parallel. Complete migration after validation.
- Migrating from monolith to microservices testing: adapt test strategy for microservices. Add contract tests for service boundaries. Add integration tests for service interactions. Reduce E2E test scope.
Compliance and Governance
- Testing SLAs: define SLAs for test execution. Unit tests complete in under 5 minutes. Integration tests complete in under 30 minutes. E2E tests complete in under 60 minutes. Communicate SLA status.
- Test reporting: generate weekly test reports. Highlight flaky tests.
- Audit and compliance: maintain audit trail of test results. Log all test environment changes.
Automation and Tooling
- Test automation framework: build a solid test automation framework. Use factory pattern for test data.
- Automated test generation: use tools for automated test generation. Generate API tests from OpenAPI specs. Edit generated tests.
- Test data automation: automate test data generation. Use seeders for database setup.
Sustainability
- Green testing: optimize test energy consumption. Reduce unnecessary test runs. Skip tests for unchanged code.
- Resource efficiency: optimize test resource usage.
- Waste reduction: reduce test waste. Remove unused test data.
Industry Standards and Frameworks
- Testing standards: follow industry testing standards. ISTQB for testing terminology. ISO/IEC 25010 for software quality. IEEE 829 for test documentation. Train team on standards.
- Test-driven development: practice TDD where appropriate. Write tests before code. Red-green-refactor cycle. Start with failing test. Write minimal code to pass. Refactor after passing.
- Behavior-driven development: practice BDD for acceptance criteria. Write scenarios in Given-When-Then format. Use BDD for user-facing features.
Reporting and Communication
- Performance reporting: generate weekly performance reports for test suites. Highlight trends and anomalies.
- Cost reporting: generate monthly cost reports for testing infrastructure. Break down by environment, tool, and team.
- Incident reporting: document all test-related incidents. Conduct post-mortem reviews. Communicate incidents to stakeholders.
Advanced Optimization
- Test suite optimization: optimize test suite for speed and reliability. Merge similar tests. Skip tests for unchanged code.
- Test environment optimization: optimize test environments for speed. Use in-memory databases. Cache environment setup.
- Test data optimization: optimize test data for speed and reliability. Use factories for on-demand data. Cache test data.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the mocking and testing guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply api mocking for testing when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Copying the example without adapting it to real data volumes and failure modes.
- Skipping load and error-injection tests before the first production deployment.
- Hard-coding values that should be configurable per environment.
- Forgetting to add logging and monitoring at each step.
- Deploying without a rollback plan or a tested backup strategy.
- Assuming the minimal example will scale without adding caching or batching.
- Not documenting the version and configuration used in production.
- Letting the recipe sit unchanged when dependencies or scale evolve.
Frequently Asked Questions
Should I mock my own service's database?
No. Use an in-memory database or TestContainers. Mock external APIs, not your own dependencies.
What's the difference between mocking and stubbing?
Stubs return canned responses. Mocks also verify interactions (was this method called with these args?).
Can mocks replace contract testing?
No. Mocks test your assumptions about the API. Contract testing verifies both sides agree on the schema.
Related Resources
CI/CD Pipeline Guide
A practical guide to building CI/CD pipelines with GitHub Actions, testing, deployment strategies, and rollback procedures.
GuideSoftware Testing Strategy Guide
A practical guide to building a layered testing strategy with unit, integration, and end-to-end tests.
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.
RecipeMock Network Requests with MSW
How to use Mock Service Worker (MSW) to intercept network requests in JavaScript tests and development, including REST and GraphQL mocking.