API Mocking and Service Virtualization for Testers

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.

Quick answerAPI mocking replaces a real API with a fake that returns predefined responses, so you can test without the real dependency, test error and edge cases on demand (500, 503, timeouts, malformed data) and start before the API exists. Service virtualization is the same idea at a larger scale: realistic, stateful simulations of whole dependent systems. Common tools: WireMock, Prism (from OpenAPI), Postman mock servers, Mockoon, MSW, and Playwright’s page.route() for mocking in UI tests. Mocks never replace a final check against the real integration.
Advertisement

Mock, Stub, Fake, Virtual Service: What’s the Difference?

TermMeaningExample
StubReturns a fixed responseGET /rates always returns {"USD": 83.2}
MockStub that also verifies how it was called“Was POST /sms called once with this phone number?”
FakeSimplified working implementationIn-memory “payment gateway” that stores payments in a map
Service virtualizationRealistic simulation of a whole dependency, often stateful, with latency and error profilesA 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)

Real-life example: travel booking A travel site depends on airline inventory, hotel suppliers, payment, SMS and email providers. Their sandboxes are slow, rate-limited and sometimes down; some charge per call. The QA team virtualizes all five for daily regression, and runs a smaller suite against real sandboxes nightly, plus a final integration check before each release.
Mock it when…Don’t mock when…
The dependency is unavailable, unstable, slow or costlyYou’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 testsThe real service is fast, stable and free to call (like the practice API)
The danger of mocks A mock is a copy of your understanding of the other API. When the real API changes, mocks keep passing while production breaks. Keep mocks honest with contract tests or by generating them from the provider’s OpenAPI spec.
Advertisement

Mocking Tools Overview

ToolTypeBest for
WireMockStandalone server / Docker / Java libraryRich request matching, stateful scenarios, fault injection, recording
Prism (Stoplight)CLI, from OpenAPIInstant mock of a spec, with request validation
Postman mock serversCloudQuick mocks from saved examples, shared with front-end teams
MockoonDesktop app / CLIGUI-friendly local mocks
MSW (Mock Service Worker)JS libraryFront-end and Node test mocking at the network layer
Playwright page.route()Built into PlaywrightMocking or modifying API calls made by the web page under test
ReadyAPI Virtualization, Broadcom/Parasoft SVCommercialEnterprise 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).

Advertisement

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.

Advertisement

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.
Advertisement

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.
Advertisement

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.

Advertisement
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.

Advertisement
View the full API testing syllabus