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.
In Python, you have a function fetch_user(user_id) that calls requests.get(...) against a live API and returns parsed JSON. Write a unit test using unittest and unittest.mock that stubs the call so the test runs entirely offline, and assert both the returned data and that the call was made with the expected URL.
Sample Answer
Direct answer
Patch requests.get for the duration of the test with unittest.mock.patch, configure the mock's return value to look like a real Response object, and assert both the parsed return value and the exact call arguments so the test proves the function calls the right URL, not just that it returns something.
Structured elaboration
from unittest.mock import patch, MagicMock
import requests
def fetch_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
import unittest
class FetchUserTest(unittest.TestCase):
@patch("requests.get")
def test_fetch_user_returns_parsed_json_and_calls_expected_url(self, mock_get):
mock_response = MagicMock()
mock_response.json.return_value = {"id": 42, "name": "Ada Lovelace"}
mock_get.return_value = mock_response
result = fetch_user(42)
self.assertEqual(result, {"id": 42, "name": "Ada Lovelace"})
mock_get.assert_called_once_with("https://api.example.com/users/42")
Executed with pytest:
py-sandbox/test_s17_fetch_user.py::FetchUserTest::test_fetch_user_returns_parsed_json_and_calls_expected_url PASSED
1 passed in 0.01s
@patch("requests.get") patches the attribute directly on the real requests module object, which is the correct target here because fetch_user calls requests.get(...) by looking up get on the requests module at call time; since there is only one requests module object in the process, patching its get attribute affects every caller, regardless of which file imported requests.
Worked example
Without assert_called_once_with, a bug where fetch_user accidentally hit .../users/{user_id}/profile or forgot to URL-encode the id would still pass a test that only checks the returned data (since the mock returns the same canned JSON no matter what URL it's called with). Asserting on the exact call arguments is what actually proves the function constructs the right request, not just that it can parse a response.
Trade-offs and pitfalls
A common mistake is patching the WRONG target, @patch("some_module_where_requests_is_imported.requests.get") only works if that's genuinely how the lookup resolves; when the code does import requests and then calls requests.get(...), patching the module attribute directly ("requests.get") is correct and more robust to refactors than patching a re-exported name. This test also completely bypasses whatever the real API would do, which is the point for a fast unit test, but it means it teaches nothing about whether https://api.example.com is even reachable or returns the shape assumed here; that's a job for a smaller number of higher-fidelity tests, not this one.
In Java, using JUnit and Mockito, write a unit test for a Service.processOrder(order) method that is expected to charge a payment gateway, save the order via a repository, and publish an order-placed event. Verify the call order and the arguments passed to each collaborator, and add a test for the case where the payment call throws: the order must not be saved or published.
Sample Answer
Direct answer
Use Mockito to inject mocks for PaymentGateway, OrderRepository, and EventPublisher, then use ArgumentCaptor and InOrder to verify both the exact arguments passed to each collaborator and the order they were called in, plus a second test that verifies nothing is saved or published when the payment call throws.
Structured elaboration
import org.junit.Test;
import org.junit.Before;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class OrderServiceTest {
@Mock PaymentGateway paymentGateway;
@Mock OrderRepository orderRepository;
@Mock EventPublisher eventPublisher;
Service service;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
service = new Service(paymentGateway, orderRepository, eventPublisher);
}
@Test
public void processOrder_chargesSavesAndPublishes_inOrder_withCorrectArguments() {
Order order = new Order("ord-1", 42.50);
service.processOrder(order);
verify(paymentGateway).charge(42.50);
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository).save(orderCaptor.capture());
assertEquals("ord-1", orderCaptor.getValue().id);
ArgumentCaptor<OrderPlacedEvent> eventCaptor = ArgumentCaptor.forClass(OrderPlacedEvent.class);
verify(eventPublisher).publish(eventCaptor.capture());
assertEquals("ord-1", eventCaptor.getValue().orderId);
InOrder inOrder = inOrder(paymentGateway, orderRepository, eventPublisher);
inOrder.verify(paymentGateway).charge(42.50);
inOrder.verify(orderRepository).save(any(Order.class));
inOrder.verify(eventPublisher).publish(any(OrderPlacedEvent.class));
}
@Test
public void processOrder_whenPaymentThrows_orderIsNeitherSavedNorPublished() {
Order order = new Order("ord-2", 10.00);
doThrow(new PaymentDeclinedException("card declined"))
.when(paymentGateway).charge(10.00);
try {
service.processOrder(order);
fail("expected PaymentDeclinedException to propagate");
} catch (PaymentDeclinedException expected) { }
verify(orderRepository, never()).save(any(Order.class));
verify(eventPublisher, never()).publish(any(OrderPlacedEvent.class));
}
}
Compiled with javac and run with JUnit 4 + Mockito 5.14.2:
JUnit version 4.13.2
..
Time: 0.5s
OK (2 tests)
(On very new JDKs, Mockito's inline mock maker needs -Dnet.bytebuddy.experimental=true until Byte Buddy officially certifies that JDK version; this is a currency detail worth knowing rather than a code defect.)
Worked example
ArgumentCaptor proves not just that save was called, but that it was called with the SAME order object (matched here by its id) that was passed into processOrder, catching a bug where the code might accidentally construct or save a different order. InOrder proves the three calls happen in the sequence the business rule requires, charge, then save, then publish, catching a bug where, say, the event gets published before the payment is confirmed to have succeeded. The second test proves the negative case: on a thrown exception from the payment gateway, verify with never() that the downstream collaborators were never touched at all.
Trade-offs and pitfalls
Verifying call order with InOrder only where the order is actually a real business requirement avoids over-specifying the test: if charge/save/publish could legitimately happen in a different sequence without being a bug, asserting a specific order makes the test brittle for no real benefit. ArgumentCaptor compares whatever equality the captured type provides, if Order doesn't override equals, comparing captured fields directly (as done here with .id) is more reliable than asserting object identity or a default equals.
You need to test payment flows that must validate idempotency and retry behavior, but you cannot call the production payment gateway from automated tests. Propose a strategy to mock or virtualize the gateway that preserves realistic behavior, including stateful idempotency tokens, duplicate-request detection, and injected errors. How would you verify that your mock is actually correct?
Sample Answer
Direct answer
Mock or virtualize the payment gateway with a stateful fake that tracks idempotency tokens and duplicate requests the same way the real gateway would, inject configurable error responses to exercise failure handling, and verify the mock's correctness by periodically checking its behavior against the real gateway's documented (or sandboxed) semantics rather than trusting it was built right once and never revisited.
Structured elaboration
- Stateful idempotency tokens: the fake gateway must remember which idempotency keys it has already seen, and return the SAME response for a repeated key rather than processing the charge twice, exactly mirroring how a real payment provider's idempotency guarantee works. A stateless fake that just always "succeeds" cannot exercise this at all.
- Duplicate-request detection: beyond idempotency keys, the fake should be able to detect and reject a genuinely duplicate charge attempt (same amount, same customer, in a short window) if that's part of what your production code is meant to guard against, so tests can verify your code's OWN duplicate-detection logic and not just the gateway's.
- Injected errors: the fake needs configurable failure modes (a decline, a timeout, a rate-limit response) that tests can select per-scenario, so retry logic, user-facing error handling, and reconciliation logic all get real test coverage.
- Verifying the mock is correct: this is the hardest and most often-skipped part. Options include periodically running the same test suite against the real gateway's sandbox environment and diffing behavior, keeping the fake's logic reviewed against the provider's published API documentation whenever it changes, or building the fake from the provider's official sandbox responses (a form of contract-based generation) rather than from memory of how it's supposed to work.
Worked example
A FakePaymentGateway stores a dictionary of idempotency_key -> response and, when charge(amount, idempotency_key) is called with a key already in the dictionary, returns the stored response unchanged instead of creating a new charge. A test configures the fake to return a "declined" response for a specific key, calls the order-processing code, and asserts the order is marked as payment_failed and never marked paid. A second test calls charge twice with the SAME idempotency key and amount, and asserts only one charge was recorded internally by the fake, proving the order code (or the fake itself, whichever owns the idempotency contract in this design) doesn't double-charge on a retried request.
Trade-offs and pitfalls
A fake payment gateway that only ever returns success teaches the team nothing about how the system behaves under decline, timeout, or duplicate-request conditions, exactly the conditions that matter most for a payment flow's correctness and are hardest to safely reproduce against a real gateway. The single biggest risk with any hand-built fake of a payment provider is confidence without verification, a fake that has silently drifted from the real gateway's actual idempotency window or error-response shape can make a whole suite pass while a real regression ships, so revisiting the fake against the provider's real documented behavior on a schedule, not just at initial build time, is part of the design.
Explain how dependency injection and programming to interfaces improve testability. Propose a small, language-agnostic pattern (constructor injection, a factory, or a service locator) you would ask engineers to adopt so that their code becomes easier to mock and supports reliable unit tests.
Sample Answer
Direct answer
Dependency injection means a class or function receives its collaborators from the outside (through a constructor parameter, a function argument, or a factory) instead of constructing or looking them up itself, and this is exactly what makes it possible to substitute a mock in tests without changing the code under test.
Structured elaboration
Three common patterns, in increasing order of flexibility:
- Constructor injection: the collaborator is passed into the constructor and stored as a field. Simplest, and makes the dependency explicit in the type signature, so it's visible at every call site.
- Factory injection: instead of injecting the collaborator directly, inject a factory function or object that can produce it, useful when the collaborator needs to be created fresh per call or configured based on runtime information not known at construction time.
- Service locator: the class asks a shared registry for its dependencies at the point of use, rather than receiving them explicitly. This is the least testable of the three, since a test now has to configure a global registry rather than simply passing in a test double, and the dependency is hidden from the type signature; prefer constructor or factory injection unless there's a specific reason (like deep call chains where threading every dependency through every layer is impractical) to fall back to a locator.
The reason this enables mocking at all: if a class reaches out and constructs its own PaymentGateway internally, a test has no seam to intercept that construction. If the PaymentGateway is instead passed in, a test can pass a mock implementing the same interface, and the class under test never needs to know or care that it's not talking to the real thing.
Worked example
A NotificationService that constructs its own SmtpClient internally (this.client = new SmtpClient(config)) cannot be unit tested without actually configuring SMTP or monkeypatching the class. Refactored to constructor injection, NotificationService(EmailClient client) accepts any object implementing the EmailClient interface; a test passes a mock EmailClient and asserts send() was called with the expected message, with zero real network activity and no SMTP configuration needed at all.
Trade-offs and pitfalls
Constructor injection can start to feel unwieldy when a class accumulates many dependencies through its constructor; that's usually a signal the class is doing too much and should be split, not a reason to reach for a service locator to hide the growing list. Programming to an interface (rather than a concrete class) is what actually enables substitution: injecting a concrete class with no interface still blocks mocking unless the mocking framework can subclass or bytecode-instrument concrete classes, which not all languages and tools support equally well.
Design tests that simulate partial failures across a distributed transaction implemented as a saga, using mocks or service virtualization for the participating services. The tests must validate idempotency, compensating actions, and correct recovery if the orchestrating process restarts mid-saga. Describe concrete test scenarios and how you would automate them safely in CI.
Sample Answer
Direct answer
Test a saga's partial-failure behavior by mocking or virtualizing each participating service so you can inject a failure at any specific step, then assert the saga correctly runs its compensating actions for every step that already succeeded, and that restarting the orchestrator mid-saga resumes (or safely re-does) the remaining work without double-processing anything.
Structured elaboration
- Idempotency: since a saga step might be retried after a restart or a timeout, each participant's mocked/virtualized behavior should be able to detect a repeated request (via an idempotency key, matching the earlier real-world pattern) and return the same result rather than performing the action twice; a test should assert this explicitly by calling a step twice and checking only one real effect was recorded.
- Compensating actions: for a scenario where step 3 of a 4-step saga fails, the test asserts that steps 1 and 2's compensating actions (a refund, a reservation release, whatever undoes that step's effect) are invoked, and that step 4 is never reached at all.
- Recovery on process restart: simulate a restart by tearing down and reconstructing the orchestrator mid-saga with only its persisted state available, then assert it correctly determines what has already happened (from that persisted state) and either resumes the remaining steps or correctly triggers compensation, without re-executing already-completed steps from scratch.
Worked example
A travel-booking saga reserves a flight, then a hotel, then charges the customer. A test configures the mocked payment service to fail, and asserts: the flight and hotel mocks each recorded exactly one "cancel/release" call (the compensating actions), and the payment mock's charge was attempted but the saga correctly marked the overall booking as failed rather than partially confirmed. A second test kills and reconstructs the orchestrator right after the flight reservation succeeds but before the hotel step runs, then asserts that on restart, the orchestrator reads its persisted state, sees the flight step already completed, and proceeds directly to the hotel step rather than re-reserving the flight.
Trade-offs and pitfalls
A saga test suite that only ever tests the happy path (every step succeeds) provides no confidence about the actual reason sagas exist, correctly handling PARTIAL failure across a distributed transaction, so failure-injection tests at every step are the real point of testing this pattern, not an afterthought. Automating the "kill and restart mid-saga" scenario safely in CI requires the test to control exactly when the simulated crash happens (right after a specific step's mock call returns, for example) rather than relying on real timing, or the test itself becomes flaky in the same way the untested production race would be.
Unlock Full Question Bank
Get access to all 34 Mocking, Stubbing, and Test Isolation interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.