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.
Give a decision rubric for when to mock an external dependency (a third-party API, a database, a cache) versus calling the real thing in a test. Name the factors your rubric weighs, and explain how the right answer can differ between a local development run, a CI pipeline, and a staging environment.
Sample Answer
Direct answer
Mock a dependency when calling the real one would make the test slow, nondeterministic, expensive, or unsafe to run repeatedly; call the real dependency when the test's whole purpose is to validate the integration itself, or when a fake would hide the exact behavior you're trying to verify.
Structured elaboration
A useful rubric weighs five factors together, not any single one in isolation:
- Speed: a network call to a real third-party service typically costs tens to hundreds of milliseconds; a thousand such tests would make the suite unusably slow. Mocking removes that cost.
- Determinism: real dependencies can be flaky (rate limits, transient errors, clock skew). A test that fails one run in twenty because of the real dependency, not because of a bug, teaches the team to ignore red builds.
- Cost: some real calls cost real money (a payment gateway, a paid data API) or consume a shared, limited quota (a sandboxed third-party account). Running them on every commit is not viable.
- Availability: local development and CI often can't reach an internal-only service, a VPN-gated system, or a partner's staging environment at all.
- Business risk: mocking a critical safety or compliance check (say, a fraud check before a payment) can mean the test never actually exercises the real decision logic, so a regression there ships silently.
Environment changes the answer: local development favors mocks for a fast inner loop; CI usually mixes a large mocked-unit-test layer with a smaller, real-dependency integration-test layer, run less frequently or gated separately; staging is often the first point where the real dependency should be exercised end-to-end, precisely because CI mocked it.
Worked example
A checkout service calls a fraud-scoring API. For the unit tests of the checkout logic (does it block the order when the score is above the threshold), mock the fraud API to return controlled scores like 0.95 and 0.10 so both branches are tested deterministically and instantly. For a smaller number of staging-only tests, call the real fraud API with known test accounts, because a mock can never tell you whether the real API's response schema or business logic changed. If the mock's shape silently drifted from the real API (say, the real API renamed a field from score to riskScore), only the staging test calling the real dependency would catch it.
Trade-offs and pitfalls
A rubric answer that only lists factors without weighing them together isn't complete: high business risk can outweigh speed and cost considerations, which is why safety-critical or compliance-relevant checks deserve at least a thin layer of real-dependency tests even if they're expensive and only run pre-release. The common mistake is mocking everything for speed and never re-validating that the mocks still match reality, which the mock's own definition can't protect against.
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 the differences between mocks, stubs, fakes, spies, and dummies. For each kind of test double, give a concrete example from a typical web application (an HTTP API call, a database, a message queue, or a cache) and a short guideline for when you would prefer that double over the others.
Sample Answer
Direct answer
A test double is a stand-in for a real dependency in a test. The five common kinds differ in what they do when called and what the test asserts about them: a dummy is passed around but never actually used, a stub returns canned data, a fake is a working but simplified implementation, a mock records calls so the test can verify they happened correctly, and a spy wraps a real object while also recording calls to it.
Structured elaboration
| Double | Behavior when called | What the test checks | Typical web-app example |
|---|---|---|---|
| Dummy | Does nothing meaningful; only fills a required parameter slot | Nothing about it directly | Passing a placeholder database-connection object into a constructor that requires one but is never queried on the code path under test |
| Stub | Returns a pre-programmed value | The return value the code under test produces | Making a "get exchange rate" HTTP client return a fixed 1.08 so a pricing calculation is deterministic |
| Fake | Runs real logic, just a lighter-weight implementation | The behavior/output, same as against the real thing | An in-memory key-value store used instead of a real Redis instance |
| Mock | Returns canned data AND records how it was called | That specific calls happened, with specific arguments, in the right order | Verifying that PaymentGateway.charge() was called exactly once with the correct amount |
| Spy | Wraps a real object, forwards calls through, and records them | Both real behavior and the interaction | Wrapping a real email-sending service so you can assert "send was called" while letting a test double catch the actual network call underneath |
The line between stub and mock is really about what the test asserts on: a stub only shapes the INPUT to the code under test (state verification), while a mock is used to verify OUTPUT in terms of interactions (behavior verification). The same test-double library (Mockito, unittest.mock, Sinon) is usually used to build all five; the taxonomy names roles, not library features.
Worked example
For a database example: a stub database client always returns a fixed list of three users for find_active_users(), regardless of what was inserted, so a report-generation test has predictable input. A mock database client would instead be used to verify that save(user) was called exactly once with a User object whose status field is "active", when testing the code that is supposed to activate a user. A fake database would be a real, lightweight in-memory dictionary-backed store that actually persists and retrieves records within the test, useful when the test needs realistic query behavior (like "insert then find") that a stub's fixed answer can't provide.
Trade-offs and pitfalls
Reach for a dummy when a dependency is only required to satisfy a constructor or function signature and is never actually exercised on the path under test; building anything more elaborate (a stub, a fake) for it would be wasted setup effort. Reach for a stub when you need to control an input; reach for a mock when the ACT of calling the dependency (with the right arguments, in the right order) is itself part of the behavior being tested, such as making sure a payment is charged exactly once. Reach for a spy when you want the dependency's real behavior to genuinely happen (unlike a mock, which fully replaces it) while still asserting that a specific interaction occurred, such as confirming a real cache was actually written to while also checking the write call's arguments. Overusing mocks where a stub would do makes tests brittle: every internal refactor that doesn't change observable behavior can still break a mock-heavy test, because the test is coupled to how the code calls its dependency rather than what the code produces.
How do you test code that depends on the current time, such as token expiry or a scheduled task? Describe the tools and approaches you would use to make the test deterministic and safe from timezone or daylight-saving issues.
Sample Answer
Direct answer
Test time-dependent code (token expiry, scheduled tasks) by injecting or freezing the clock rather than letting the code call the system clock directly, so the test controls exactly what "now" is and the result is deterministic and safe from timezone or daylight-saving surprises.
Structured elaboration
- Clock injection: pass a clock (a function, an interface, or an object with a
now()method) into the code under test instead of callingdatetime.now()orSystem.currentTimeMillis()directly. In tests, supply a fake clock you fully control; in production, supply the real system clock. This is the most explicit and testable approach, and it composes well with dependency injection generally. - Freeze-time libraries: tools like Python's
freezegunor similar libraries in other languages monkeypatch the standard time functions for the duration of a test, so code that calls the system clock directly (without being refactored for injection) still becomes deterministic. Convenient when you can't change the production code's clock access. - Timezone and daylight-saving safety: always reason and assert in UTC internally, and only convert to a local timezone at the presentation boundary. Explicitly test boundary dates (a daylight-saving transition, a leap day, a year rollover) since these are exactly the inputs where naive "add N hours" or "add N days" arithmetic silently breaks.
Worked example
A token-expiry check computes is_expired = now() > issued_at + timedelta(hours=1). With a fake clock fixed at issued_at + 59 minutes, the test asserts is_expired is false; advancing the fake clock to issued_at + 61 minutes and asserting is_expired is true tests both sides of the boundary deterministically, with no real waiting and no flakiness from how fast the test happens to run.
Trade-offs and pitfalls
Freeze-time libraries are convenient but can hide a design smell: if a large codebase relies on monkeypatching the system clock everywhere because nothing was ever built to accept an injected clock, that's a sign the code should be refactored toward explicit clock injection over time, since it also makes production behavior (like clock skew across servers) easier to reason about. A subtler pitfall is testing only "normal" days; DST and leap-year boundaries are where naive time arithmetic actually breaks, and a suite with no tests near those boundaries can pass indefinitely while carrying a real bug.
Explain the roles of unit, integration, and end-to-end tests in a delivery pipeline. For each layer, describe what should typically be mocked versus run against a real dependency, and walk through a concrete example for a payment flow showing where mocks belong and why.
Sample Answer
Direct answer
As a rough default: unit tests mock everything outside the function or class under test; integration tests run against real (or lightly virtualized) adjacent components but still mock the furthest-out third parties; end-to-end tests run against real dependencies wherever it's safe and affordable to do so.
Structured elaboration
- Unit tests: the fastest, most numerous layer. Everything the unit under test calls, database, network, other services, gets mocked, so the test isolates and exercises only the logic in that one unit.
- Integration tests: verify that two or more of YOUR OWN components wire together correctly (your service and your database, your service and an internal message queue). Real (or a very high-fidelity fake of) your own infrastructure is used here, but external third parties are usually still mocked or virtualized, since the point is validating your own integration code, not the third party's uptime.
- End-to-end tests: exercise a full user-facing flow across real systems. Real external dependencies are used where safe (sandboxed accounts, staging environments); anything unsafe, costly, or nondeterministic to call for real (an actual bank transfer, an actual SMS bill) still gets stubbed even at this layer.
Worked example
For a payment flow: at the unit level, mock the PaymentGateway interface entirely and test that the order service calls charge() with the right amount and handles a thrown exception by not saving the order. At the integration level, run the order service against a real local database to confirm the SQL actually persists correctly, while still mocking the payment gateway (a third party, out of scope for this layer). At the end-to-end level, run the full checkout flow against the payment gateway's real sandbox environment, so the test confirms the actual network contract and response shapes match what your code expects, without charging a real card.
Trade-offs and pitfalls
The three-layer split breaks down if a team pushes everything into unit tests with mocks and skips the integration layer entirely: unit tests can all pass while the wiring between your own components is broken, because no test ever exercised your actual database or actual message-queue configuration. Conversely, pushing too much into end-to-end tests makes the suite slow and flaky without necessarily catching bugs any earlier or more precisely than a well-placed integration test would.
Unlock Full Question Bank
Get access to all 10 Mocking, Stubbing, and Test Isolation interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.