API Mocking and Service Virtualization for Testers
Your checkout tests fail every morning because the payment gateway’s sandbox is down. The front-end team is blocked because the new orders API won’t be ready for three weeks. You need to test what happens when the shipping partner returns a 503, but you can’t make their real server fail on demand. Mocking and service virtualization solve all three. In this lesson you’ll learn when to mock and when not to, the main tools, and how to build a controllable fake API inside a Playwright test.
page.route() for mocking in UI tests. Mocks never replace a final check against the real integration.Mock, Stub, Fake, Virtual Service: What’s the Difference?
| Term | Meaning | Example |
|---|---|---|
| Stub | Returns a fixed response | GET /rates always returns {"USD": 83.2} |
| Mock | Stub that also verifies how it was called | “Was POST /sms called once with this phone number?” |
| Fake | Simplified working implementation | In-memory “payment gateway” that stores payments in a map |
| Service virtualization | Realistic simulation of a whole dependency, often stateful, with latency and error profiles | A virtual core-banking system for an entire QA environment |
In everyday conversation people say “mock” for all of these. In interviews, knowing the distinction earns points.
When to Mock (and When Not To)
| Mock it when… | Don’t mock when… |
|---|---|
| The dependency is unavailable, unstable, slow or costly | You’re testing the integration itself (contract, auth, real data formats) |
| You need error cases on demand (500, 503, timeout, bad JSON) | The mock would just repeat your assumptions about the other system |
| The API doesn’t exist yet (develop and test from the spec) | It’s the final pre-release check |
| You need deterministic data for UI tests | The real service is fast, stable and free to call (like the practice API) |
Mocking Tools Overview
| Tool | Type | Best for |
|---|---|---|
| WireMock | Standalone server / Docker / Java library | Rich request matching, stateful scenarios, fault injection, recording |
| Prism (Stoplight) | CLI, from OpenAPI | Instant mock of a spec, with request validation |
| Postman mock servers | Cloud | Quick mocks from saved examples, shared with front-end teams |
| Mockoon | Desktop app / CLI | GUI-friendly local mocks |
| MSW (Mock Service Worker) | JS library | Front-end and Node test mocking at the network layer |
Playwright page.route() | Built into Playwright | Mocking or modifying API calls made by the web page under test |
| ReadyAPI Virtualization, Broadcom/Parasoft SV | Commercial | Enterprise service virtualization (SOAP, MQ, mainframe) |
WireMock in 5 Minutes
docker run -d -p 8080:8080 --name wiremock wiremock/wiremock
Create a stub by POSTing a mapping to WireMock’s admin API (or by placing JSON files in its mappings folder):
curl -X POST http://localhost:8080/__admin/mappings -H "Content-Type: application/json" -d '{
"request": { "method": "POST", "urlPath": "/payments", "bodyPatterns": [ { "matchesJsonPath": "$[?(@.amount > 100000)]" } ] },
"response": { "status": 402, "jsonBody": { "error": "limit_exceeded" }, "fixedDelayMilliseconds": 300 }
}'
# Fault injection: simulate a broken connection
curl -X POST http://localhost:8080/__admin/mappings -H "Content-Type: application/json" -d '{
"request": { "method": "GET", "urlPath": "/shipping/rates" },
"response": { "fault": "CONNECTION_RESET_BY_PEER" }
}'
WireMock also records real traffic into stubs (proxy mode), runs stateful scenarios (first call “pending”, second call “paid”) and lets you verify which requests were received (/__admin/requests).
Prism: A Mock Server from an OpenAPI File
npx @stoplight/prism-cli mock openapi.yaml # http://127.0.0.1:4010
curl -i http://127.0.0.1:4010/users/1 # returns the spec's example or a generated value
curl -i "http://127.0.0.1:4010/users/1" -H "Prefer: code=404" # ask for a specific documented response
Because Prism validates incoming requests against the spec, it also catches front-end bugs (wrong field names, missing required fields) before the real backend exists.
Hands-On: A Controllable Fake API Inside Playwright
For API-level tests of a client or service, you can start a tiny HTTP server in a fixture, tell it what to return per test, and assert how your code reacts. No extra tools needed. Here a fake payment gateway returns success, a 503 with Retry-After, or a slow response:
import { test as base, expect } from '@playwright/test';
import http from 'node:http';
import type { AddressInfo } from 'node:net';
type Reply = { status: number; body?: unknown; headers?: Record<string, string>; delayMs?: number };
type FakeGateway = { url: string; next: (...replies: Reply[]) => void; calls: { method: string; path: string; body: string }[] };
const test = base.extend<{ gateway: FakeGateway }>({
gateway: async ({}, use) => {
const queue: Reply[] = [];
const calls: FakeGateway['calls'] = [];
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
calls.push({ method: req.method!, path: req.url!, body });
const reply = queue.shift() ?? { status: 500, body: { error: 'no stub' } };
setTimeout(() => {
res.writeHead(reply.status, { 'Content-Type': 'application/json', ...reply.headers });
res.end(reply.body === undefined ? '' : JSON.stringify(reply.body));
}, reply.delayMs ?? 0);
});
});
await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
const { port } = server.address() as AddressInfo;
await use({ url: `http://127.0.0.1:${port}`, next: (...r) => queue.push(...r), calls });
server.close();
},
});
// The code under test: a payment client that retries once on 503 using Retry-After
async function chargeWithRetry(request: any, url: string, amount: number) {
for (let attempt = 1; attempt <= 2; attempt++) {
const res = await request.post(`${url}/payments`, { data: { amount }, timeout: 2_000 });
if (res.status() !== 503 || attempt === 2) return res;
await new Promise((r) => setTimeout(r, Number(res.headers()['retry-after'] ?? 1) * 1000));
}
}
test('successful charge', async ({ request, gateway }) => {
gateway.next({ status: 201, body: { id: 'pay_1', status: 'captured' } });
const res = await chargeWithRetry(request, gateway.url, 2297);
expect(res.status()).toBe(201);
expect(JSON.parse(gateway.calls[0].body)).toEqual({ amount: 2297 });
});
test('retries once after 503 with Retry-After, then succeeds', async ({ request, gateway }) => {
gateway.next(
{ status: 503, body: { error: 'maintenance' }, headers: { 'Retry-After': '1' } },
{ status: 201, body: { id: 'pay_2', status: 'captured' } },
);
const res = await chargeWithRetry(request, gateway.url, 500);
expect(res.status()).toBe(201);
expect(gateway.calls).toHaveLength(2);
});
test('slow gateway makes the client time out', async ({ request, gateway }) => {
gateway.next({ status: 201, body: {}, delayMs: 3_000 });
await expect(chargeWithRetry(request, gateway.url, 100)).rejects.toThrow(/Timeout/);
});
All three tests pass. The same fixture pattern works for SMS, email, shipping or any dependency: queue the replies you need, run your code, then assert both the outcome and the calls the fake received.
Mocking API Calls in UI Tests with page.route()
When testing a web page, Playwright can intercept the browser’s API calls and answer them itself. This is ideal for showing rare UI states such as empty lists, errors and slow loading:
import { test, expect } from '@playwright/test';
test('orders page shows a friendly error when the API fails', async ({ page }) => {
await page.route('**/api/orders*', (route) =>
route.fulfill({ status: 503, json: { error: 'maintenance' }, headers: { 'Retry-After': '30' } }));
await page.goto('https://your-app.test/orders');
await expect(page.getByText('We are having trouble loading your orders')).toBeVisible();
});
test('modify a real response: force an empty cart', async ({ page }) => {
await page.route('**/api/cart', async (route) => {
const response = await route.fetch(); // call the real API
const json = await response.json();
json.items = []; // change only what we need
await route.fulfill({ response, json });
});
await page.goto('https://your-app.test/cart');
await expect(page.getByText('Your cart is empty')).toBeVisible();
});
Replace the URLs with your application’s. page.route() affects only that page’s browser traffic; it doesn’t change what the request fixture sends. Playwright can also replay recorded traffic from a HAR file with page.routeFromHAR().
Keeping Mocks Honest
- Generate mocks from the provider’s OpenAPI spec (Prism) or from recorded real traffic (WireMock record mode, HAR files).
- Run contract tests so the provider proves the mocked responses are still real.
- Keep mock data realistic: real field names, formats, sizes, and error bodies copied from the real API.
- Run a smaller suite against the real integration regularly (nightly or pre-release).
- Version mocks with the tests in Git and review changes like code.
Test Ideas Only Mocks Make Easy
- Every documented error code from a dependency (400, 401, 403, 404, 409, 429, 500, 502, 503, 504).
- Timeouts, connection resets, partial or empty bodies, invalid JSON.
- Very slow responses (does the UI show a spinner? does the backend time out properly?).
- Unusual but valid data: 0 items, 10,000 items, very long names, Unicode, null optional fields.
- State sequences: payment “pending” → “pending” → “captured” across polls.
- Dates and times: end of month, leap day, daylight saving changes, time zones.
Interview Questions: API Mocking and Service Virtualization
What is API mocking?
Replacing a real API dependency with a fake that returns predefined responses, so tests can run without the real service and can simulate any scenario, including errors and delays.
What is the difference between a stub and a mock?
A stub returns canned responses. A mock also records and verifies how it was called, for example that an SMS API received exactly one request with a specific phone number.
What is service virtualization?
Simulating the behaviour of entire dependent systems, often stateful and with realistic latency and error profiles, so teams can develop and test when the real systems are unavailable, costly or hard to control.
When should you not use mocks?
When testing the actual integration, contracts, authentication and real data formats, and for final pre-release verification. Over-reliance on mocks can hide real integration problems.
Which mocking tools have you used?
Examples: WireMock for stateful stubs and fault injection in Docker, Prism to mock from OpenAPI, Postman mock servers for front-end teams, and Playwright page.route() to mock API responses in UI tests.
How does page.route() work in Playwright?
It intercepts network requests made by the page that match a URL pattern; the handler can fulfill them with a custom response, modify a real response fetched with route.fetch(), continue them with changes, or abort them.
How do you keep mocks in sync with the real API?
Generate them from the provider’s OpenAPI spec or recorded traffic, use contract tests to verify the provider still matches, keep realistic data, and regularly run tests against the real integration.
How would you test a retry mechanism?
Use a mock that returns failures (e.g. 503 with Retry-After) followed by success, then assert the client waits the right time, retries the expected number of times, doesn’t retry non-idempotent calls unsafely and eventually succeeds or fails cleanly.
What is fault injection?
Deliberately introducing failures such as errors, delays, dropped connections or malformed data into dependencies to verify the system handles them gracefully. WireMock supports it directly.
How can testing start before the backend is ready?
Agree an OpenAPI spec, run a mock server from it (Prism or Postman), write tests and front-end code against the mock, then point the same tests at the real API when it’s deployed.