Mocking, Stubbing, and Test Isolation Questions
Isolating the unit under test from its dependencies. Covers mocks, stubs, fakes, and spies, when to use test doubles versus real dependencies, and controlling external services and time. Includes designing for isolation so tests are fast, deterministic, and focused.
You are writing integration tests for a service that depends on a rate-limited third-party API. Propose a checklist of practices to keep the tests stable and safe to run in CI without exceeding the provider's rate limits or causing real side effects. Explain which parts of your checklist rely on mocking versus other techniques, and how you would categorize existing tests by how much they depend on that external dependency.
Sample Answer
Direct answer
Keep integration tests stable against a rate-limited third-party API by combining mocking for the bulk of tests, a small number of real calls that respect the provider's limits, circuit breakers and retries in the code under test itself, and a way to categorize tests by how much they actually depend on the external service, rather than treating "call the real API" and "mock it" as the only two options.
Structured elaboration
A practical checklist:
- Mock the dependency for the majority of tests: any test whose purpose is your own business logic, not the third party's real behavior, should not touch the real API at all.
- Throttle deliberately for the small number of tests that do call the real API: add explicit rate limiting or spacing in the test suite itself (not just relying on the provider's own limit response) so your CI run never approaches the provider's ceiling and never impacts their real operations.
- Use stubbing and replay for realistic failure scenarios: a controllable stub or a recorded-and-replayed real response lets you exercise rate-limit (429) or downtime (5xx/timeout) responses on demand, without needing the real provider to actually be rate-limiting or down at test time.
- Test your resilience mechanisms directly: circuit breakers, retries, and feature flags that gate a feature off if the dependency is unhealthy should have their own tests, using the stub to simulate the exact failure conditions those mechanisms exist to handle.
- Categorize tests by dependency: tag or group tests by which external dependency they touch and how (mocked-only versus real-call), so a provider outage or rate-limit change only threatens a known, small, clearly-labeled set of tests, and the team can quickly decide whether to skip that group temporarily without guessing which tests are affected.
Worked example
A weather-data integration has 40 tests total. 35 are mocked entirely and run on every commit. 5 are tagged @real_api and run only in a nightly job, spaced 2 seconds apart to stay well under the provider's per-minute rate limit and specifically chosen to validate things a mock cannot: that the real response schema still matches what the code expects, and that a real 429 response is handled by the circuit breaker exactly as designed. A separate stub-based test simulates a 429 explicitly to verify the circuit breaker opens after 3 consecutive failures, without needing the real provider to actually be rate-limiting at that moment.
Trade-offs and pitfalls
Treating rate-limit or intermittent third-party slowness purely as a "make it more reliable" problem and mocking everything removes the flakiness but also removes the team's only signal that the real integration still works; the categorization step (which tests actually call the real dependency) is what prevents that risk from becoming invisible. A checklist that never mentions respecting the provider's actual limits, only the test suite's own convenience, risks getting your test account rate-limited or banned, which is a self-inflicted outage of your own CI.
Explain mocking versus stubbing versus service virtualization for integration tests. For each technique, describe when you would use it, its benefits and limitations in terms of fidelity versus control, how it affects long-term test maintenance, and how you would fit it into a CI workflow for a backend dependency.
Sample Answer
Direct answer
Mocking, stubbing, and service virtualization sit on a spectrum of increasing fidelity and decreasing control: a mock/stub replaces a dependency inside your process with a hand-written, minimal fake; service virtualization runs a separate, more realistic fake service (often generated from a real API contract) that your code talks to over the network exactly as it would talk to the real thing.
Structured elaboration
- Mocking/stubbing (in-process): you replace the dependency object itself. Fast, fully under your control, zero network involved. Best when the dependency's real network behavior (latency, serialization quirks, auth handshakes) isn't what the test cares about.
- Service virtualization (out-of-process): a separate process (or container) responds to real HTTP/gRPC calls with configured or recorded responses. Slower to start than an in-process mock, but exercises your real network/serialization code path, and can be shared across multiple test processes or even multiple teams.
- Deciding factor #1: what you're testing. If the unit under test's logic is what matters, mock. If you're testing that your HTTP client is wired correctly, that timeouts are handled, or that a whole flow works end-to-end without hitting a real third party, virtualize.
- Deciding factor #2: parallel execution. When many tests run in parallel, in-process mocks are cheap to instantiate one-per-test with no shared state to worry about; a single virtualized service instance can become a bottleneck or a source of cross-test interference unless it's stateless or given per-test isolation (a separate container, a request-scoped state key).
- Deciding factor #3: fidelity risk. A hand-written mock can silently drift from what the real dependency actually does. Service virtualization built from a real API contract (an OpenAPI spec, a recorded interaction) is less likely to drift, but only if that contract is kept current.
- Long-term maintenance. A hand-written mock is code you own forever: every time the real dependency's contract changes, someone has to remember to update the mock by hand, and nothing forces that to happen. A virtualized service generated from (or periodically checked against) a real contract shifts that burden toward keeping ONE shared contract current rather than every team's individual mock, which scales better as the number of consumers grows.
- Fitting into a CI workflow. In-process mocks need nothing extra: they live and die with the test process, so they parallelize for free across CI workers. Service virtualization needs an explicit CI step, starting the virtualized service (or a per-worker instance of it) before the test suite runs, tearing it down after, and giving each parallel CI worker its own instance or its own isolated state so two workers' tests don't interfere through a shared virtualized service.
Worked example
Testing a "look up shipping cost" feature that calls a shipping-rate API: for a fast unit test of the pricing math, mock the shipping client to return a fixed $4.99 and assert on the total. For an integration test verifying your retry-on-5xx logic actually retries, run a lightweight virtualized shipping service (for example a small local server serving canned responses) that you can configure per-test to return a 503 once and then a 200, so you exercise the real HTTP call and real retry code path. Mocking would hide a real bug here: if your retry logic accidentally retries on a 4xx instead of a 5xx, a mock that never returns a real HTTP status object at all wouldn't catch it, while the virtualized server, hit over real HTTP, would.
Trade-offs and pitfalls
Don't default to virtualization for everything: it adds process/container startup cost and a maintenance surface (keeping the virtual service's responses realistic). Don't default to mocking for everything either: a suite that only ever mocks the shipping client will never notice that your retry logic has a bug in exactly the code path a mock skips over. The safest posture is to use mocks for the majority of unit tests and reserve service virtualization for the specific integration tests whose job is to validate the boundary itself.
Compare the main strategies for virtualizing an external service in tests: hand-written contract-based stubs, a standalone mock server generated from an API contract, and network-level record-and-replay of real traffic. For each, discuss maintenance overhead, fidelity to production behavior, and how it scales when many tests run in CI. How would you decide it is safe to rely on one of these instead of the real dependency?
Sample Answer
Direct answer
Contract-based stubs, generated mock servers, and record-and-replay virtualization trade off differently on maintenance cost, fidelity, and scalability: hand-written contract stubs are cheapest to write but drift fastest, a mock server generated from a real API contract stays truer to the interface with less manual upkeep, and record-and-replay captures real traffic faithfully but is the most brittle to any change in the real service.
Structured elaboration
- Hand-written contract-based stubs: a developer writes the expected request/response pairs directly. Cheap to start, fast to run, but nothing keeps them honest, if the real service's contract changes, the stub keeps returning the old shape and tests keep passing against a lie.
- Standalone mock server generated from an API contract (for example, from an OpenAPI spec) via a tool like WireMock: the mock server's shape is derived mechanically from a document that is, ideally, kept current alongside the real API. This reduces manual drift risk versus hand-written stubs, at the cost of needing the contract itself to be trustworthy and current, and some setup/startup cost since it's a separate running process rather than an in-process object. Configuring stateful scenarios (a resource that behaves differently on a second call) and keeping the generated stubs in sync with schema changes are the ongoing maintenance tasks.
- Network-level record-and-replay: real traffic is captured once against the actual service, then replayed verbatim in tests. Highest fidelity to actual real-world behavior at the moment of recording, but the most brittle over time: any change to the real service (even a benign one) can make the recording stale, and non-deterministic fields in the recorded response (timestamps, request ids) need explicit handling or the replay will fail on exact-match comparisons. In CI it scales about as well as an in-process stub, since replay usually reads from local fixture files with no separate server process to start, but the fixture files themselves accumulate in the repository over time and must be scoped per test (not shared globally) so one test's replayed interaction cannot be consumed or exhausted by another test running in parallel.
- A fourth point on the spectrum, worth naming: spinning up a real but disposable instance of the dependency (a throwaway container running the actual service, or a sandboxed real instance) trades away some speed and isolation for the highest possible fidelity, and is most appropriate when the dependency is your own team's service rather than a genuine third party.
Deciding it's safe to rely on one of these instead of the real dependency comes down to how the fidelity gap is monitored: pairing whichever technique you choose with periodic revalidation against the real service (a scheduled job that diffs the contract, or an occasional smoke test against the real dependency) is what actually keeps any of the three honest over time.
Worked example
A team virtualizing a shipping-rates API starts with hand-written stubs for speed, then migrates to a WireMock server generated from the provider's published OpenAPI spec once the hand-written stubs drift and cause a production incident (the real API added a required field the stub never returned). They still keep one recorded-and-replayed interaction from a real sandbox call specifically to catch response-shape drift the OpenAPI spec itself might miss (an undocumented field the provider actually sends), and schedule a monthly job that re-records it and diffs against the version checked into source control.
Trade-offs and pitfalls
Startup cost matters in CI: an in-process mock or stub is essentially free to instantiate per test, while a standalone mock server has real startup latency and, if shared across parallel test workers, needs per-test isolation (a unique port, a request-scoped state key) to avoid cross-test interference. UI/e2e tests calling out to a virtualized dependency have their own practical concerns, realistic response shapes, managing test data and auth tokens the mock needs to accept, and keeping the mock layer updated as the real API evolves are ongoing work, not a one-time setup cost. Isolating tests from a genuinely flaky downstream dependency is itself a valid motivating reason to reach for any of these three, on top of the pure speed argument.
For large-scale performance and load testing, argue when using mocked components is acceptable and when you need real dependencies. Propose a hybrid plan that mixes mocked and real services to measure system capacity and find bottlenecks, while keeping test cost and time reasonable and avoiding conclusions that a fully mocked run could not support.
Sample Answer
Direct answer
For large-scale performance and load testing, mocking is acceptable only for components that are genuinely not part of what you're trying to measure (a notification service whose latency doesn't affect the checkout path under load, for example); for anything on the critical path whose real-world behavior under load is exactly the unknown you're testing, a mocked stand-in can produce a capacity number that doesn't hold up in production.
Structured elaboration
The core risk: a mock typically responds instantly and with unlimited concurrency, so a load test that mocks a real bottleneck (a database, a downstream service with its own capacity limits) will measure the capacity of everything EXCEPT that bottleneck, and can report a headline throughput number far higher than the system could actually sustain in production once the real dependency's own limits kick in.
A hybrid plan:
- Identify the dependencies actually on the critical path for the metric you're measuring (throughput, p99 latency, error rate under load) and keep those real, at least in a representative capacity (a real database sized similarly to production, a real downstream service in a load-test environment with its own realistic scaling).
- Mock dependencies genuinely off the critical path, or ones you've deliberately decided to measure independently in a separate, targeted load test.
- Validate the mocked dependencies' assumed latency and error-rate profile against real production or staging data, so even a mocked component's behavior in the load test reflects a realistic response-time distribution rather than an unrealistic instant response.
- Explicitly state, in the load test's own documentation or output, which dependencies were real and which were mocked, so anyone reading the resulting capacity numbers knows exactly what was and wasn't measured, and doesn't accidentally treat a partial measurement as a full-system capacity guarantee.
Worked example
Load-testing a checkout service, keep the real payment gateway's sandboxed load-test environment in the loop (since its actual latency and rate limits under load are part of what determines real checkout throughput), but mock the email-receipt service entirely (since its latency genuinely doesn't gate the checkout response, it's fired asynchronously after the user already sees success). Reporting "checkout throughput: 5,000 requests/second" without disclosing the email service was mocked is fine, since it's genuinely off the critical path; reporting the same number while the PAYMENT gateway was mocked would be actively misleading, since the real bottleneck was never exercised.
Trade-offs and pitfalls
Running the full system with every real dependency at true production scale is often prohibitively expensive or logistically impossible (rate limits on a payment provider, cost of provisioning production-scale infrastructure just for a test), which is exactly why the hybrid plan matters: it's not "mock everything for cost" or "use nothing but real dependencies for accuracy", it's a deliberate, documented choice per dependency based on whether it gates the metric being measured.
Your organization runs polyglot microservices across several languages, and two teams' independently hand-written mocks of the same internal service have already drifted out of sync with each other and with the real service. Propose an approach to keep mocking and stubbing consistent across languages so cross-service tests stay trustworthy as the organization grows, and explain how you would enforce it in CI.
Sample Answer
Direct answer
Keep mocking consistent across a polyglot organization by defining the contract once, in a language-neutral format like OpenAPI or JSON Schema, and generating each language's mocks or stub servers from that single source, rather than letting every team hand-write and independently maintain its own language-specific mock implementation of the same dependency.
Structured elaboration
- Shared contract formats: OpenAPI (for HTTP/REST APIs) or JSON Schema (for message payloads generally) act as the single source of truth for what a dependency's interface actually looks like, independent of which language calls it or which language implements it.
- Code generation: generate typed client stubs and mock/test-double scaffolding for each language directly from the shared contract, so a Java consumer's mock and a Python consumer's mock of the SAME dependency are structurally guaranteed to agree, since they're both derived from the same source rather than hand-maintained separately.
- Language-agnostic stub servers: for cases where an in-process, language-specific mock isn't practical (a polyglot integration test spanning multiple services), run one standalone mock server (again driven by the shared contract) that any language's HTTP client can call identically, removing the need for a separate mock implementation per language entirely.
- CI orchestration: wire contract regeneration into CI so that when the shared contract changes, every consuming language's generated mocks are regenerated and any breaking change surfaces as a build failure in each affected language's pipeline, rather than silently drifting per-language.
Worked example
A payment-service contract is defined once in OpenAPI. A Java team generates typed mock stubs from it using a code-generation tool tied to their build; a Python team generates an equivalent stub client from the same OpenAPI document using a Python-specific generator; a Node.js team, needing to test against the service from a UI test, points at a shared, contract-driven mock server instance instead of writing yet another hand-rolled mock. When the contract adds a new required field, regenerating from the updated OpenAPI document surfaces a compile-time or type-check failure in the Java and Python generated clients immediately, rather than each team discovering the mismatch independently, at different times, in different ways.
Trade-offs and pitfalls
Code generation from a shared contract only helps if the contract itself is kept current and treated as the actual source of truth, if any team's hand-written override silently diverges from the generated baseline "just this once," the whole point of sharing one contract is undermined for that team. Orchestrating regeneration across multiple languages' CI pipelines is real coordination work, the payoff (consistent behavior, one place to fix a contract bug) is worth it specifically at the scale where several languages already independently maintain overlapping, drifting mocks of the same dependency; for a two-service, single-language shop, this is more machinery than the problem justifies.
Unlock Full Question Bank
Get access to all 30 Mocking, Stubbing, and Test Isolation interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.