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.
Using pytest, write a parameterized test for a /calculate API endpoint that covers multiple input cases. The endpoint calls an external rate-limit or billing service; mock that call so the tests do not depend on the network. Show the test code demonstrating both the parametrization and the mock.
Sample Answer
Direct answer
Use pytest.mark.parametrize to run the same test body across multiple input cases, and use the requests_mock fixture to stub the external rate-limit or billing check so none of the parametrized cases make a real network call.
Structured elaboration
import pytest
import requests
BILLING_CHECK_URL = "https://billing.internal.example.com/check"
def calculate(a, b, op):
check = requests.get(BILLING_CHECK_URL)
check.raise_for_status()
if not check.json().get("allowed", False):
raise PermissionError("rate limit or billing check failed")
if op == "add":
return a + b
if op == "sub":
return a - b
if op == "mul":
return a * b
if op == "div":
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
raise ValueError(f"unsupported op: {op}")
@pytest.mark.parametrize(
"a,b,op,expected",
[
(2, 3, "add", 5),
(10, 4, "sub", 6),
(3, 5, "mul", 15),
(9, 2, "div", 4.5),
],
)
def test_calculate_happy_paths(requests_mock, a, b, op, expected):
requests_mock.get(BILLING_CHECK_URL, json={"allowed": True})
assert calculate(a, b, op) == expected
assert requests_mock.call_count == 1
def test_calculate_denied_by_billing_check(requests_mock):
requests_mock.get(BILLING_CHECK_URL, json={"allowed": False})
with pytest.raises(PermissionError):
calculate(1, 1, "add")
Executed with pytest:
test_calculate_happy_paths[2-3-add-5] PASSED
test_calculate_happy_paths[10-4-sub-6] PASSED
test_calculate_happy_paths[3-5-mul-15] PASSED
test_calculate_happy_paths[9-2-div-4.5] PASSED
test_calculate_denied_by_billing_check PASSED
5 passed
The requests_mock fixture (from the requests-mock pytest plugin) intercepts any requests call made during the test and returns the configured response; it's reset automatically between parametrized cases, so each case gets a fresh mock with no leftover call count or state from the previous one.
Worked example
The parametrization covers four arithmetic operations in one test function instead of four near-identical copy-pasted tests, and the billing-check mock is configured identically in each parametrized case, proving the endpoint's core math is correct across operations while the "denied by billing" case, a separate non-parametrized test, proves the endpoint correctly refuses to calculate anything when the external check disallows it. Note the endpoint's design here: the billing check happens before any arithmetic, so a division-by-zero case still needs the billing mock configured, otherwise the test would fail on the billing check rather than reaching the division logic being tested.
Trade-offs and pitfalls
A parametrized test with a mocked call assumes each configured mock response is representative of what the real service can return; if the real billing service can return additional fields the code doesn't check yet (like a retry_after value on a 429), the parametrized test won't surface that gap since it never expected to test it. Keep parametrized cases focused on truly interchangeable inputs (different operation/operand combinations here); if a case needs a meaningfully different mock setup, it usually belongs as its own explicit test rather than being force-fit into the same parametrize list.
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.
Describe test isolation in the context of automated testing for a microservice. Explain why isolation matters, list common sources of test interference, and outline the practices you would enforce in CI to keep tests isolated.
Sample Answer
Direct answer
Test isolation means one test's setup, execution, and teardown cannot affect another test's outcome; it matters because a suite where tests interfere with each other produces failures that depend on run order or which tests happened to run before it, which destroys the ability to trust a single test's result in isolation.
Structured elaboration
Common sources of interference in a microservice's test suite:
- Shared mutable state: a database, an in-memory cache, or a static/global variable that one test writes to and another test reads, so the second test's outcome depends on what the first test did.
- External resources with real state: a shared file, a shared queue, or a shared third-party sandbox account whose state persists across test runs.
- Nondeterministic inputs: the current wall-clock time or a source of randomness that produces a different value each run, so the same test can pass or fail depending on when or how many times it's run.
- Order dependence: a test that only passes because an earlier test happened to leave the system in a particular state, and fails if run alone or in a different order.
Practices to enforce isolation in CI: give each test (or each test worker, for parallel execution) its own database transaction that gets rolled back, or its own ephemeral schema/namespace; inject the clock and any randomness sources instead of reading them from the environment directly, so tests can pin them; run tests that must touch shared infrastructure serially or with explicit locking rather than assuming parallel safety by default; and treat "passes alone but fails in the full suite" as a bug in the test, not a fluke to re-run away.
Worked example
A microservice's order-processing tests all write to the same test database table without transactions. Test A creates an order with id 1001; test B, checking "creating a duplicate order id fails," happens to reuse 1001 and only passes because test A ran first and left that row behind. Wrapping each test in a transaction that rolls back at teardown, or generating a fresh unique id per test, removes the order dependence entirely: each test now sets up exactly the state it needs and leaves nothing behind for the next one.
Trade-offs and pitfalls
Enforcing isolation has a real cost (transactions, ephemeral environments, injected clocks add setup code), and it is tempting to skip it while the suite is small. The cost of NOT doing it compounds: as the suite grows, order-dependent tests accumulate silently until a routine reordering (parallelizing the suite, or adding a new test earlier in the file) causes a wave of failures with no obvious cause, which is far more expensive to untangle after the fact than to prevent up front.
Discuss lifecycle management for test doubles in a large test suite: how they get created and configured, how you verify they were used correctly, and how they get reset and torn down between tests. Propose naming and fixture-scope conventions to avoid leaking mock state between tests and keep mocks consistent across a team.
Sample Answer
Direct answer
Manage a test double's lifecycle explicitly: create and configure it per test (or per fixture scope), verify it was used as expected during the test, and reset or tear it down afterward so no leftover configuration or recorded state leaks into the next test, especially under parallel execution.
Structured elaboration
A leftover mock is a concrete, common failure mode: if test A configures a mock to always return a specific value and the test framework reuses the same mock instance for test B without resetting it, test B can silently pass or fail based on test A's leftover configuration rather than its own setup, exactly the kind of order-dependence test isolation is meant to prevent, and it gets worse when tests run in parallel and two tests race to configure the same shared mock at once.
Conventions that keep this manageable at scale:
- Naming conventions: consistent naming for mock variables and their setup helpers so it's obvious, reading a test, which mocks exist and what they're configured to do.
- Fixture scope: most testing frameworks let you declare whether a fixture (including a mock) is created fresh per test, per test class, or per test session; default to per-test scope for mocks unless there's a clear, deliberate reason (like an expensive-to-construct fake) to share one, and if you do share one, reset it explicitly between tests.
- Reset semantics: use the mocking framework's built-in reset capability (clearing recorded calls and configured return values) rather than manually reconstructing state, so resets stay consistent as the suite grows.
- Verification: check a test double was used correctly with the mocking framework's own interaction-verification API rather than inspecting internal state by hand, assert on exact call counts and arguments (
mock.assert_called_once_with(...),verify(mock).method(args)), assert on calls that must NOT have happened (assert_not_called,verify(mock, never())), and, when call order is itself a real requirement, assert it explicitly (InOrder/assert_has_callswithany_order=False) rather than only checking the final return value.
Worked example
A test suite that shares one mock EmailClient instance across all tests in a file (for speed) must call the framework's reset function in a setUp/beforeEach hook, clearing both the configured return values and the recorded call history, so a later test asserting "send was called exactly once" isn't accidentally counting a call made by an earlier test that reused the same mock instance without resetting it.
Trade-offs and pitfalls
Sharing a mock instance across tests for speed trades away some safety: it's faster to construct once, but every test now depends on the reset logic being complete and correctly wired into the test lifecycle, and a single missed reset can produce confusing, hard-to-reproduce failures that only show up depending on test execution order. Under parallel test execution, sharing isn't safe at all unless the mock (and its reset) is somehow made thread-safe or each parallel worker gets its own instance; the simplest and most robust default is a fresh mock per test.
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.
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.