Articles
Mockito Interview Questions: Spies, Strict Stubbing, and Static Mock Scope
Practice Mockito interview questions with tested spy repairs, strict stubbing diagnostics, matchers, and static mock thread-scope examples for Java engineers.
Mockito interview questions become easier when you identify when a failure happens. A spy can execute real code while you are arranging a stub. Strict stubbing can reject a call whose arguments disagree with that arrangement. A static mock can work on the test thread and have no effect on a worker thread. Those are three different problems, with three different fixes.
This guide uses Mockito 5.23.0, JUnit Jupiter 5.12.2, and Java 21 to examine a small shipping-quote example. Official documentation establishes the library contracts; the execution results below come from an original local practice project. These are preparation exercises, not reports of a particular employer's interview.
Start With the Test Boundary
A mock supplies controlled behavior for a collaborator. A spy uses real behavior for methods you have not stubbed. That makes a spy useful when preserving part of an existing object's behavior, but it also makes arranging the test potentially consequential. A method that reads a file, indexes a collection, or opens a connection does not become harmless because the object is a spy.
Our quote service adds a 100-cent handling charge to a rate returned for the EU region:
interface Rates { int cents(String zone); }
static int quote(Rates rates) {
return rates.cents("EU") + 100;
}
The test controls the rate; the service still performs the addition. If you stub the quote method itself and assert its configured answer, you have removed the behavior you wanted to examine. A useful interview explanation names the collaborator being replaced, the real code still running, and the outcome being asserted.
The local project uses MockitoExtension and a JUnit BOM to align the Jupiter dependencies. Its Surefire JVM explicitly loads the matching Mockito core JAR through -javaagent. Mockito's official documentation describes explicit instrumentation for Java 21 and later; it is an environment concern to diagnose separately from a failed business assertion. A dependency mismatch or missing agent is not evidence that the shipping calculation is wrong.
Why Does Stubbing a Spy Call the Real Method?
Consider a rate table whose first entry has not been populated:
class Table {
private final List<Integer> rows = List.of();
int first() { return rows.get(0); }
}
var table = spy(new Table());
when(table.first()).thenReturn(700);
Java must evaluate table.first() before passing its result into when. Because the spy has no replacement for that method yet, the real implementation indexes the empty list. In our run, arrangement failed with ArrayIndexOutOfBoundsException, before any quote assertion ran.
The minimal repair changes the stubbing form:
var table = spy(new Table());
doReturn(700).when(table).first();
assertEquals(700, table.first());
This repair passed locally. It avoids executing first() during arrangement. It does not populate the underlying list, repair the table's initialization contract, or prove that a production rate loader works.
The official Mockito spy documentation recommends the doReturn family for cases where calling the real method during stubbing is unsuitable. Do not turn that recommendation into a rule that every mock must use doReturn. Ordinary when(...).thenReturn(...) is clearer for many mocks and provides useful compile-time type checking.
A strong follow-up answer also questions whether the spy belongs here. If the test is about loading rate rows, use a real table with deliberate input. If it is about a consumer's reaction to a supplied rate, a small Rates mock may express the boundary more directly. The choice follows what you are testing, rather than a preference for one Mockito syntax.
Which Configuration Makes Stubbing Strict?
With the pinned MockitoExtension integration, the default strictness is STRICT_STUBS. That statement comes from the versioned MockitoSettings source. It should not be broadened into “every call to mock() always validates unused stubs.” Runners, rules, extensions, sessions, and explicit settings determine the applicable lifecycle and validation.
Here is the relevant test setup:
@ExtendWith(MockitoExtension.class)
class QuoteTest {
@Mock Rates rates;
}
Strictness is valuable because an incorrect arrangement can make a test misleading even when the final assertion looks plausible. Before weakening it, read both locations in the failure: where the stub was declared and where the collaborator was invoked. Then compare the method, arguments, and intended branch.
Argument mismatch: the caller requests EU
when(rates.cents("US")).thenReturn(700);
assertEquals(800, quote(rates));
The original practice project puts the quote implementation in a separate source file. Its call to cents("EU") disagrees with the US stub. The observed failure was PotentialStubbingProblem.
If EU is the intended region, the repair is:
when(rates.cents("EU")).thenReturn(700);
assertEquals(800, quote(rates));
verify(rates).cents("EU");
If the requirement actually says US, changing the stub would conceal a production bug; change the implementation instead. Mockito can identify disagreement, but it cannot decide the business requirement for you. Explain that decision before editing.
Do not promise that every argument disagreement produces the same exception under every arrangement. Mockito's diagnostics use context, and some patterns of repeated stubbing need a different stubbing API. This exercise records the result of this specific cross-file call with the stated extension configuration.
Unused stub: the expected path never runs
when(rates.cents("EU")).thenReturn(700);
assertEquals(2, 1 + 1);
The arithmetic assertion passes, but the extension reports UnnecessaryStubbingException after the test. Nothing consumed the rate stub. That is a different signal from a call that consumed the wrong arguments.
The smallest cleanup is deleting the unused arrangement. However, the resulting arithmetic test still does not exercise shipping behavior. In a real review, either replace it with the intended quote test or remove an irrelevant test entirely. Passing after cleanup is evidence that the unnecessary stub is gone, not that coverage improved.
Our deliberately broken suite ran seven tests and reported three errors: the spy arrangement, argument mismatch, and unused stub. Running the three repaired examples plus the static-scope test produced four passing tests with no failures or errors. Keep broken exercises outside your default CI selection; their failure is intentional.
When Is lenient Appropriate?
Use leniency for an intentional exception whose reason you can explain, such as a shared optional collaborator arrangement that is genuinely useful across several paths. Prefer narrowing the exception to that stub. First ask whether moving setup into the test that needs it would remove the exception altogether.
lenient().when(rates.cents("EU")).thenReturn(700);
This line changes validation, not the shipping requirement. It cannot make an incorrect region correct. Nor does it guarantee an assertion will pass: an unstubbed invocation can still return a default value that produces the wrong result.
An interview answer should describe the information being discarded. Broad class-level leniency may quiet an entire suite while hiding obsolete setup. A localized exception with a clear reason is easier to revisit when the collaborator changes.
Matchers Select Calls; Captors Explain Calls
A matcher describes which invocation should receive configured behavior. A captor collects an argument from a verified invocation so that the test can inspect it afterward. Start with exact values when they express the contract; use a matcher when multiple values are intentionally equivalent for this test.
For example, replacing "EU" with anyString() would make the rate stub accept the wrong region too. That might be appropriate when region selection is outside the test's scope, but it weakens a test specifically intended to verify routing. Do not widen a matcher merely to silence a mismatch.
For a collaborator with multiple arguments, Mockito requires matcher usage to be consistent within the invocation. If one argument uses a matcher, represent a literal with eq(...) where needed. Also distinguish a typed matcher from a null expectation rather than assuming they mean the same thing.
A captor is helpful when the system constructs a request object whose fields matter. Verify the collaborator call, capture that request, and assert the destination and charge. If the only important argument is the string EU, verify(rates).cents("EU") already communicates the requirement without extra machinery.
Does a Static Mock Follow Work Onto Another Thread?
Official contract: a MockedStatic affects the thread on which it was created. Its control object is not safe to operate from another thread, and closing it releases the mock. Use try-with-resources to give that lifetime an explicit boundary.
The practice class returns "US" from its real static method. Inside the owner's scope, the stub returns "EU":
try (var mocked = mockStatic(Region.class)) {
mocked.when(Region::current).thenReturn("EU");
assertEquals("EU", Region.current());
try (var pool = Executors.newSingleThreadExecutor()) {
assertEquals("US",
pool.submit(Region::current).get());
}
}
assertEquals("US", Region.current());
The worker invokes the static method; it does not receive or manipulate the MockedStatic handle. The explicit executor also makes the thread change visible instead of leaving it to an incidental scheduling choice.
| Invocation in the local exercise | Observed result | Explanation |
|---|---|---|
| Owner, before opening scope | US | Real implementation |
| Owner, inside scope | EU | Thread-local stub |
| Worker, while scope is open | US | Worker is outside that thread-local mock |
| Owner, after close | US | Real implementation restored |
This distinction matters when a synchronous unit test passes but asynchronous application code behaves differently. First trace the execution thread. Increasing a timeout does not move a static mock onto the worker.
Our preparation recommendation is to consider an injected region provider when asynchronous code needs controlled configuration. That creates an explicit dependency both threads can use under an appropriate design. It is a design suggestion, not a claim that every static method must be rewritten or that injection automatically provides thread safety.
Explain What Your Test Still Does Not Prove
A passing quote test establishes behavior under its chosen rate. It does not establish database connectivity, serialization compatibility, live rates, retries, or concurrency correctness. Those concerns require their own boundaries and, where appropriate, integration tests with real dependencies.
During a follow-up, give a concrete missing case. For this service, ask what should happen if the provider throws, returns a negative rate, or receives an unsupported region. Choose assertions from the intended contract. Avoid asserting every incidental interaction; a harmless refactor should not require rewriting tests that protect unchanged behavior.
| Question | Practice focus |
|---|---|
| Explain How You Would Test a System | Separate unit evidence from integration evidence. |
| Fix failing tests and refactor code | Diagnose before broadening a stub. |
| Implement buffered file writer with concurrency support | Explain thread boundaries and observable effects. |
| Implement a Database Connection Pool | Decide which lifecycle behavior needs real dependencies. |
| Explain Java follow-ups after solving coding problem | Defend implementation assumptions in Java. |
For your next Backend Engineer practice session, take one failure and explain its timing, cause, minimal repair, and remaining uncertainty aloud before changing the code.
Sources and Further Reading
- Mockito 5.23.0 API documentation in source — spies, matchers, captors, inline instrumentation, and strictness guidance.
- MockitoSettings 5.23.0 — extension strictness default.
- MockedStatic 5.23.0 — thread ownership and closing contract.
- Mockito project — supported configuration and project documentation.