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 examples of using mocks or stubs to simulate exception and error conditions, such as a network timeout, a database constraint violation, or a message-broker disconnect. Explain why deliberately exercising these error paths in tests is valuable for product reliability, beyond just covering the happy path.
Sample Answer
Direct answer
Use mocks or stubs to make a dependency fail on demand, a network timeout, a database constraint violation, a message-broker disconnect, so error-handling code gets exercised deliberately in every test run, rather than only occasionally when something happens to actually break in a shared test environment.
Structured elaboration
- Network timeout: configure a stub HTTP client to raise a timeout exception instead of returning a response, and assert the calling code handles it (retries, falls back, or surfaces a clear error) rather than crashing or hanging.
- Database constraint violation: configure a stub or fake repository to raise a unique-constraint-violation exception on
save, and assert the calling code translates that into the right domain-level outcome (e.g., "this order already exists" rather than an unhandled 500). - Message-broker disconnect: configure a fake message publisher to raise a connection error, and assert the calling code either retries the publish, queues it for later, or at minimum doesn't silently lose the message without any indication.
Worked example
A checkout flow's happy-path test never encounters a database constraint violation naturally, since the fixture data is always fresh, so without deliberately injecting one, the "duplicate order" error-handling branch is completely untested. Configuring the repository stub to throw that specific exception on the second save call lets a test walk through exactly that branch and assert on the resulting user-facing error message, deterministically, on every run.
Trade-offs and pitfalls
The value here isn't just "does the error not crash the program", it's whether the SPECIFIC handling is correct: does a timeout trigger the intended retry policy, does a constraint violation map to the right domain error rather than a generic failure, does a broker disconnect actually get retried or surfaced rather than silently dropped. Deliberately exercising error paths is what catches the class of bug that only shows up in production during a real outage, when nobody wants to be discovering error-handling gaps for the first time.
Mocking can both reduce and introduce test flakiness. Describe at least three ways this happens in practice, and for each give a mitigation an SDET could apply to keep the suite stable and predictable over time.
Sample Answer
Direct answer
Mocking reduces flakiness by removing dependence on a real, sometimes-unreliable external system, but it introduces flakiness of its own when the mock itself behaves nondeterministically, is configured with real timing assumptions, or silently drifts from what the real dependency now does, causing intermittent failures that have nothing to do with the code actually being tested.
Structured elaboration
Three concrete ways mocking cuts both directions:
- Reduces flakiness by removing real network/timing variance: a stubbed HTTP call returns instantly and deterministically instead of sometimes being slow or occasionally erroring for reasons unrelated to the code under test. Mitigation: keep leaning on this, it's the main reason to mock in the first place, for tests whose purpose is the calling code's logic rather than the real dependency's behavior.
- Introduces flakiness through shared or leftover mock state: if a mock is reused across tests without being reset, or shared across parallel workers, one test's configuration or recorded calls can leak into another, producing failures that depend on execution order or parallelism, not a real bug. Mitigation: give each test its own freshly configured mock, and if a mock is shared across tests for performance, reset its configuration and recorded calls explicitly between tests rather than assuming a clean slate.
- Introduces flakiness through timing-sensitive mock setup: a mock configured with a real (small) artificial delay, or asynchronous mock callback wiring that isn't properly awaited, can behave inconsistently depending on how the test runner schedules things, even though nothing about the mock's real intent was random. Mitigation: avoid baking real artificial delays into a mock's configuration unless the test's actual purpose is exercising timeout/slow-response handling, and make sure every async mock callback is properly awaited or returned as a promise so the test runner cannot proceed before the mock has actually resolved.
- Introduces silent DRIFT-based flakiness: a mock that hasn't been revisited in months can silently diverge from what the real dependency now returns; this doesn't show up as flaky in the traditional sense (the mocked test itself stays perfectly stable), but it means the suite is stably testing against a fiction, and the real-world flakiness shows up later, in production, when the divergence finally matters. Mitigation: schedule a small, periodic set of tests (or a contract check) that run against the real dependency and compare its actual shape to the mock's assumptions, so drift surfaces on a known cadence rather than only being discovered when it finally causes a production incident.
Worked example
A suite that mocks a shipping-rate API stays perfectly green for months. The real API later adds a required authentication header; nothing in the mocked suite notices, since the mock never validated headers in the first place. The suite is not "flaky", it's stably wrong, until a smaller number of real-dependency tests (or production itself) surfaces the drift, at which point it looks like sudden, confusing flakiness in whatever depends on that integration.
Trade-offs and pitfalls
Treating "the suite is green" as proof mocking has fully solved flakiness misses the drift case entirely, since drift produces silent staleness, not visible flakiness, until something finally forces the gap into view. Pairing heavy mocking with periodic revalidation against the real dependency (even a small number of real-call tests, or a scheduled contract check) is what keeps the reduced-flakiness benefit from quietly becoming a false sense of security.
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.
Explain approaches to intercept and modify network requests and responses during a Selenium test in order to simulate backend conditions. Compare using an HTTP proxy, browser-level network interception, and a service-virtualization tool, and provide a short example showing how you would stub a single JSON API response.
Sample Answer
Direct answer
Three approaches intercept network traffic during a Selenium test at different layers: an HTTP proxy sits between the browser and the network and can rewrite any request/response; browser-level network interception (via the Chrome DevTools Protocol) hooks directly into the browser's own network stack; and a service-virtualization tool replaces the backend entirely with a configurable fake server the browser talks to normally.
Structured elaboration
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| HTTP proxy (e.g. a local proxy the browser is configured to route through) | Sits outside the browser, intercepts and can rewrite any request/response crossing it | Works across any browser or client, language-agnostic | Extra process to run and configure, and HTTPS interception needs a trusted certificate installed in the browser |
| Browser-level interception via the Chrome DevTools Protocol | The test driver registers request/response handlers directly with the browser's own devtools connection | No separate process, no certificate trust issues, very precise control per-request | Chromium-specific (or needs an equivalent for other engines), and ties the test to the devtools API surface |
| Service virtualization | The backend the app calls is a real, separately configurable server | Exercises the app's real network code path against a realistic backend, reusable across UI and API tests | Slower to set up, requires the app to be pointed at the virtual server's URL instead of production |
A short example stubbing a single JSON API response (shown here with an HTTP-level interception library so it is genuinely runnable without a browser; the identical idea applies whether the interception happens via a proxy or the Chrome DevTools Protocol):
import responses
import requests
def fetch_profile(user_id):
resp = requests.get(f"https://app.example.com/api/profile/{user_id}")
resp.raise_for_status()
return resp.json()
@responses.activate
def test_stub_a_single_json_api_response():
responses.add(
responses.GET,
"https://app.example.com/api/profile/77",
json={"id": 77, "plan": "pro"},
status=200,
)
result = fetch_profile(77)
assert result == {"id": 77, "plan": "pro"}
assert len(responses.calls) == 1
Executed with pytest:
test_s13_network_interception.py::test_stub_a_single_json_api_response PASSED
1 passed in 0.03s
Worked example
Testing a profile page that should show a "Pro" badge only for pro-tier users: stub the GET /api/profile/77 call to return {"plan": "pro"} and assert the badge renders; stub the same endpoint to return {"plan": "free"} in a second test and assert the badge does not render. Neither test depends on a real backend being up, and both are deterministic regardless of what the real profile service currently returns for user 77.
Trade-offs and pitfalls
An HTTP proxy adds a genuinely separate moving part (the proxy process itself, HTTPS certificate trust) to the test environment; browser-level interception avoids that but is coupled to whichever browser engine's devtools protocol you're using; service virtualization is the heaviest but also the most representative of real end-to-end behavior. Whichever layer is chosen, the stubbed response shape needs to be kept realistic, if the real API adds a required field the stub never includes, the UI test can pass while the real integration is actually broken.
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.
Unlock Full Question Bank
Get access to all 17 Mocking, Stubbing, and Test Isolation interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.