API Mocking and Service Virtualization

API mocking replaces an API that the system under test depends on with a stand-in that answers its HTTP requests as the test requires. For example, an order service calls a payment provider. In the test environment the provider's base URL points to a stub server that answers POST /v1/charges with "captured", "declined" or 503, depending on the card token sent, so every payment path can be tested without moving real money.

Service virtualization applies the same idea to a whole test environment: complete dependent systems are simulated, often from recorded traffic, and shared by several teams. The ISTQB Foundation syllabus lists stubs, drivers, simulators and service virtualization among the test environment items prepared during test implementation. Contract testing defined what an API promises; a good mock keeps exactly those promises. The test doubles of component testing (stub, spy, mock, fake) now move from code to the network.

Why it matters

Real dependencies often stop testing: another team's service is not built yet, a third-party sandbox is down, calls cost money or hit a rate limit, and shared data changes between runs. Tests that depend on them fail for reasons unrelated to the code under test.

Above all, doubles make error paths testable. A real payment provider will not return a 503, a 429 or a reply after 15 seconds on request, yet those are the moments when customers are charged twice or orders are lost. The price: a double is only as good as its resemblance to the real API; a wrong one gives green tests and failures in production.

Stubs, mocks and fakes when the dependency is an API

The system under test sends real HTTP requests, and something other than the real service answers them. Only configuration changes: the dependency's base URL, for example PAYMENT_API_URL, points to the double in CI and to the real provider in staging. "Mock" is used loosely in API work, and a "mock server" is usually configured as a stub, so ask what the double actually does:

  • Stub: returns a canned response for each request it matches, such as GET /v1/rates/RUH → 200 {"fee": 18}, and remembers nothing. The test checks your system's result, such as the fee shown (state verification).
  • Mock: answers like a stub, but is also pre-programmed with expectations about the requests it must receive, such as exactly one POST /v1/charges with an Idempotency-Key header; the test fails if the call is missing, repeated or different (behaviour verification). A test that inspects the request log afterwards uses the double as a spy.
  • Fake: a simplified working implementation with its own state, usually in memory: POST /v1/orders stores an order and GET /v1/orders/{id} returns it. Useful when calls depend on each other, but its own logic can be wrong.
  • Service virtualization: a simulated dependent system in a shared test environment, often recorded from real traffic, with state, delays and failures, sometimes beyond HTTP. It costs more to build and maintain, and a platform or test-environment team often owns it. Some teams use the term as a synonym for API mocking; the difference is scope and ownership.
Behind an API base URL, a stub only answers, a mock also checks the requests it receives, a fake keeps its own state, and service virtualization simulates a whole system for a shared environment. Conceptual model

Stub

Answers with canned responses

Example:
GET /v1/rates/RUH → 200 {"fee": 18}
Remembers:
Nothing: the same request always gets the same answer
The test checks:
Your system’s result (state verification)
Watch out for:
Drift from the real API

Mock

Answers and checks the requests

Example:
Exactly one POST /v1/charges with an Idempotency-Key header
Remembers:
The requests it received
The test checks:
The requests your system sent (behaviour verification)
Watch out for:
Expectations so strict that harmless changes break tests

Fake

A simplified working implementation

Example:
POST /v1/orders stores an order; GET /v1/orders/{id} returns it
Remembers:
Its own data, usually in memory
The test checks:
Your system’s result across calls that depend on each other
Watch out for:
Its own logic can be wrong too

Service virtualization

A simulated dependent system

Example:
Recorded real traffic, replayed with state, delays and failures
Remembers:
State across calls, as recorded or configured
The test checks:
Whole flows in a shared test environment
Watch out for:
Cost and upkeep; recordings may hold personal data and tokens

Typical roles: tools and teams often use the names loosely, so ask what a double actually does.

Try it: Mock API Response Generator Try it: Mock JSON Generator

When to mock, and when to use the real API

Decide per dependency and per test level: replace what you do not own or control, and use the real thing for what the test must prove. Never replace the system under test itself. Keep the real dependency or the provider's sandbox for system integration and end-to-end checks. Never stub the security control under test: a stub that accepts any token proves nothing about authentication. A double pays off when:

  • The dependency does not exist yet: a stub built from an agreed OpenAPI document lets testing start early.
  • It is a third party that is slow, charges per call, limits the request rate or has no usable sandbox.
  • You need responses the real service will not give on demand: 429 with Retry-After, 503, a timeout, a body that is not JSON, an empty list.
  • Tests must be fast and repeatable in CI, whatever the state of shared environments.

Worked example: payment failures at checkout

The payment provider's sandbox returns only a successful charge or a declined card. Requirement R-21: "A declined card leaves the order UNPAID and shows the reason. If the provider is busy or unavailable, the order stays UNPAID and the customer is asked to try again later. With no answer within 10 seconds, the order becomes PAYMENT_PENDING; its charge status is checked later, and it is never charged again." The test environment points the payment base URL at a stub server, one stub per card token:

  • Only the first two rows can be produced with the sandbox; without stubs, the other four would most likely be seen for the first time in production, by real customers.
  • tok_slow needs more than a stub: besides the order status, the test verifies that exactly one charge request arrived, even after the customer presses Pay again, either as a mock expectation or by reading the request log afterwards (a spy).
  • tok_broken is not in R-21: it came from the tester's question "what if the body is not JSON?", and the team chose to treat it like an unavailable provider.
Stub server for the payment provider (test environment only)
Request: POST /v1/charges; the stub is chosen by "token" in the body

token         Stub response                         Expected in our system
------------  ------------------------------------  ------------------------------
tok_ok        201 {"status":"captured"}             order PAID, receipt sent
tok_declined  422 {"error":"card_declined"}         order UNPAID, reason shown
tok_busy      429, Retry-After: 30                  order UNPAID, "try again later"
tok_down      503 {"error":"unavailable"}           order UNPAID, "try again later"
tok_slow      201, but only after 15 s              at 10 s: PAYMENT_PENDING and
                                                    exactly one charge request
tok_broken    200, body is an HTML error page       handled as unavailable, logged

Try it: API Status Code Simulator Try it: Mock Error Response Generator

Risks of mocking and how to contain them

A double is an assumption about another system, written as test code. Each risk is a way that assumption goes wrong:

  • Drift: the provider renames a field or changes its error format, the double keeps the old answer, and production fails while the tests stay green. Validate stub bodies against the provider's contract, and run contract tests that send the same requests to the provider's sandbox and compare its answers with the doubles', on a schedule such as once a day and at each provider release. Consumer-driven contracts that the provider verifies close the gap from the other side.
  • Unrealistic behaviour: doubles answer instantly and never rate-limit. Add realistic delays where timing matters, and keep a few tests on the real service.
  • Over-mocking: so many doubles that only the doubles are tested, or expectations so strict about headers and call order that harmless changes break tests.
  • Leaks: a stub URL left in a staging or production configuration. Each environment should state which dependencies are real, and deployment checks should confirm it.
  • Recorded data: traffic recorded for service virtualization can contain personal data and access tokens; mask it before storing or sharing it.

Try it: API Schema Validator Try it: Response Contract Comparator

Common mistakes

  • Calling every double a mock without agreeing what it does: a stub that only answers cannot detect a duplicate charge.
  • Writing stub responses from memory instead of from the contract or a recorded real response.
  • Stubbing only the success path, so timeouts, retries and error messages are never exercised.
  • Treating a green suite that runs on doubles as proof that the integration works.
  • Leaving doubles without an owner, so nobody updates them when the provider changes.
  • Committing recorded traffic with personal data or live tokens to the repository.

The QA perspective

Testers often decide what the doubles return, because the valuable responses are the error cases they design. In refinement, ask for each external dependency: is there a sandbox, which errors and limits does the contract document, how will we simulate a timeout, and who owns the stubs? Record the answers per test level in the test plan.

In reports, state whether a result came from a double or from the real service; a pass on a stub is weaker evidence. Attach the stub definition to bug reports. A disagreement between a double and the real API is a finding: either the double is wrong or the provider broke its contract. After a production incident involving a dependency, add that response to the stubs.

Practice exercise

Plan the doubles for a shipping-rate API

Your task

A checkout shows delivery options from a shipping provider. Using requirement CHK-7 and the contract excerpt below, plan the stubs, decide which statement needs a mock rather than a stub, and say what still needs the real sandbox and how you will detect drift.

Material
Requirement CHK-7 (checkout, delivery step)
1. Show the delivery options sorted by fee, cheapest first. If the provider
   returns no options, show "Delivery is not available to this address".
2. If the provider does not answer within 5 seconds, or answers with a 5xx
   status, offer the fixed fee of 25.00 SAR and log the event.
3. Cache the options per city and weight for 10 minutes: one provider call
   per city and weight, not one per page view.

Provider contract (excerpt)
GET /v2/rates?city={code}&weightKg={number}
200  {"rates": [{"service": "standard", "fee": 18.00, "days": 3}, ...]}
400  {"error": "invalid_city"}
429  Retry-After: {seconds}
Steps
  1. List the provider responses that exercise each statement of CHK-7, errors included.
  2. Write a stub table: request, stub response, expected checkout behaviour.
  3. Mark the scenario that needs request verification and what the test asserts.
  4. List what still needs the real sandbox, and one way to detect drift.
Show the model answer
Model answer

Statement 1: 200 with options deliberately unsorted (35.00, 18.00, 22.00) → 18.00 first, because sorted stub data cannot catch a missing sort; 200 with "rates": [] → "Delivery is not available to this address".

Statement 2: 503 → fixed fee 25.00 and a log entry; a reply after 7 seconds → fixed fee and a log entry; a reply after about 4 seconds → the provider's options, so the limit is tested from both sides.

Not in CHK-7: 400 invalid_city and 429 with Retry-After. Stub them anyway and ask the product owner what each should show.

Statement 3 needs a mock or a spy: the page looks the same with or without the cache, so only the request log shows the caching. Three visits for one city and weight → exactly one GET /v2/rates; a visit after 10 minutes, or with the test clock moved forward → exactly one more; the same city with another weight → its own call, because a cache keyed by city alone would show the wrong fees.

Real sandbox: a few tests proving that the provider accepts the real request (parameter names, authentication) and that its response parses. Drift: validate the stub bodies against the provider's schema in CI, and compare a fresh sandbox response with the stubs at each provider release.

Frequently asked questions

Is a mock server the same as a mock?

Not strictly. A mock server that only returns canned responses is acting as a stub. It acts as a mock, or a spy, when the test also checks which requests it received. Tools use the words loosely, so describe what the double does rather than arguing about its name.

What is the difference between API mocking and service virtualization?

Mostly scope and ownership. API mocking usually means stubs or mocks that a team defines for its own tests. Service virtualization simulates whole dependent systems inside a shared test environment, often from recorded traffic and with state, delays and failures, and is often run by a platform or test-environment team. Some teams use the two terms interchangeably.

If we use mocks, do we still need contract tests and end-to-end tests?

Yes. Mocks let you test your system in isolation and force the error paths. Contract tests check that the mocks still match the real API. A few tests against the real integration, in staging or the provider's sandbox, confirm that the wiring, authentication and configuration work. Each covers what the others cannot.

Can a tester work with mocks without writing code?

Often, yes. Many mock servers are configured with JSON or YAML files or through a user interface, and a stub is essentially a request to match and a response to return. Mutqan's mock tools generate response bodies and error payloads for such files, and the API Status Code Simulator gives you an endpoint that returns any status code, with an optional delay of up to 10 seconds and a custom JSON body. That is enough to test a client's error handling, and its timeouts when they are shorter than 10 seconds.

References

  • ISTQB Certified Tester Foundation Level (CTFL) v4.0ISTQB · 2023The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
  • Test Double (Martin Fowler)martinfowler.com · 2006Gerard Meszaros's vocabulary for test doubles — dummy, fake, stub, spy and mock — as summarised by Martin Fowler.
  • Mocks Aren't Stubs (Martin Fowler)martinfowler.com · 2007The difference between state verification and behaviour verification, and between the classical and mockist styles of test-driven development.
  • Contract Test (Martin Fowler)martinfowler.com · 2011Contract tests: periodic checks that the calls made against a test double return the same results as the same calls to the real external service.

Practise with Mutqan tools

Generate complete mock API responses (status, headers, JSON body) for success, list, detail, error and empty cases.

Mocking

Generate structured error responses (validation, auth, not found, rate limit, server error) in popular formats.

Mocking

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

Generate mock JSON from a template with placeholders ({{name}}, {{uuid}}, {{int:1-10}}, {{date}}, {{pick:a|b}}).

Mocking

Validate API responses against a JSON Schema or an OpenAPI response schema.

API Testing

Compare an actual API response against an expected contract (example or schema) and report breaking vs non-breaking differences.

Validation

Go deeper

Your progress on this topic

Part of the QA Roadmap