skip to content

With a Mockito mock created using RETURNS_DEEP_STUBS, how do you verify an interaction on the last object of a call chain, and why are invocation counts on the intermediate objects untrustworthy?

level: seniorimportance: should knowfreq 20%

answer

  1. verify the leaf — chain returns the cached child
  2. navigation is itself a recorded call on the parent
  3. stubbing + verify(...) argument inflate link counts
  4. never use times()/verifyNoMoreInteractions on links
  5. strict stubs flag partly exercised chains

basics

~20 s

Verify on the leaf: verify(config.getServer()).restart() works because getServer() returns the cached child mock. But every call to getServer() — from production code, from your stubbing, and from inside the verify argument — is recorded on the parent, so times(n) on chain links and verifyNoMoreInteractions are misleading.

solid answer

~60 s

Verification works on the **leaf** of the chain, because navigating the chain returns the same cached child mock: ```java verify(config.getServer()).restart(); ``` Mockito verifies `restart()` on the child mock, which is exactly the object the code under test used. The unreliable part is the *links*. Every traversal of `config.getServer()` is a real invocation recorded on `config` — including the ones your test performs while stubbing (`when(config.getServer().getPort())…`) and the one inside the `verify(...)` argument itself. So `verify(config, times(1)).getServer()` counts your test's own bookkeeping alongside the production calls, and `verifyNoMoreInteractions(config)` will typically fail or pass for reasons that have nothing to do with the code under test. Practical rules: assert on the leaf only; hold the child in a local variable so the navigation is explicit and happens once; do not put count-based verification on chain links; and avoid `verifyNoMoreInteractions` on deep-stubbed mocks entirely. If you genuinely need to assert that the intermediate call happened a specific number of times, that is the signal to stop deep-stubbing and inject the collaborator directly.

code

java · 9 lines
java
Config config = mock(Config.class, Answers.RETURNS_DEEP_STUBS);
when(config.getServer().getPort()).thenReturn(8080);

new Restarter(config).restartIfPortChanged(9090);

Server server = config.getServer();   // cached child
verify(server).restart();
// verify(config, times(1)).getServer();          // meaningless
// verifyNoMoreInteractions(config);              // will not hold

go deeper

for a junior

Know that you verify on the last object in the chain, e.g. verify(config.getServer()).restart().

for a middle

Explain the child-mock caching that makes leaf verification work, and that navigating the chain is itself a recorded call.

for a senior

Lead with the pollution problem: counts on links and verifyNoMoreInteractions are meaningless, so assert on outcomes and the leaf only.

for a principal

Treat the need to verify an intermediate link as a design signal — inject the collaborator instead of mocking a path to it.

## Why leaf verification works at all A deep stub caches the child mock it creates for each invocation. So when your test writes `verify(config.getServer()).restart()`, the argument `config.getServer()` is evaluated first and returns the *same* mock instance the production code received. `verify(...)` then wraps that child, and `.restart()` is checked against its recorded invocations. Nothing special is happening: you are verifying an ordinary mock that you happened to obtain by navigating. The readable form is to name it: ```java Server server = config.getServer(); verify(server).restart(); verify(server).setPort(8080); ``` This also stops you from re-traversing the chain once per assertion. ## Why the links are polluted The traversal itself is a method call on the parent mock, and Mockito records *every* call on a mock — it cannot distinguish "the test navigating" from "the system under test collaborating". A typical deep-stub test therefore calls `config.getServer()` at least three times: once while stubbing, once (or more) inside the production code, and once inside each `verify(...)` argument. The consequences: - `verify(config, times(1)).getServer()` fails, or passes only by coincidence, and the number changes when you add an assertion — an assertion whose result depends on how you wrote your other assertions is worthless. - `verifyNoMoreInteractions(config)` is essentially unusable, because the mock has recorded your test's navigation calls plus the auto-created stubbings. - `verify(config, never()).getServer()` can only be trusted if the chain is never navigated in the test either, which defeats the point. - Argument-sensitive links compound this: `repo.find("a")` and `repo.find("b")` are different invocations with different children, so a count on `find` mixes populations you probably meant to separate. A related sharp edge: you cannot verify "the chain was navigated" and "the leaf was called" as one atomic fact. Mockito has no notion of a chained interaction; it has a parent with recorded calls and a child with recorded calls. ## Interaction with strictness Under `MockitoExtension` with the default `Strictness.STRICT_STUBS`, stubbing a chain that the code under test never fully exercises reports `UnnecessaryStubbingException`, because the leaf stubbing was never used. That is correct and useful, but it means a partially exercised chain surfaces as a strictness error rather than a plain assertion failure — confusing the first time you see it. Argument mismatch behaves the same way: stub `find("a")`, call `find("b")`, and you get a fresh unrelated child mock plus a strict-stubs complaint, not an obvious failure at the point of divergence. ## What to do instead The honest reading is that deep stubs are a *stubbing* convenience, not a *verification* tool. Use them to get data into the system under test, and assert on outcomes — the returned value, the state change, the single interaction with the real collaborator you care about. When a test genuinely needs to assert how many times an intermediate object was requested, the object graph traversal is behaviour you are testing, and the right move is to inject that collaborator directly rather than mock your way down to it: ```java // instead of deep-stubbing config.getServer().restart() new Restarter(server).restart(); // server injected, verify(server).restart(); ``` That also removes the strict-stubs noise and makes the test independent of the graph's shape.

  • Why does verify(config.getServer()).restart() work, given that config.getServer() is an unstubbed call?
    It is not really unstubbed: the deep stub recorded the auto-created child as a stubbing keyed on that invocation, so re-navigating returns the identical mock the production code used. `verify(...)` then operates on that ordinary child mock. The one side effect is that the navigation adds another recorded invocation to the parent.
  • Is verifyNoMoreInteractions safe to use on a deep-stubbed mock?
    No. The parent has recorded every navigation your test performed while stubbing and verifying, so it almost never holds, and when it does hold the reason is accidental. If a test needs that guarantee, stop deep-stubbing the mock and inject the collaborator you actually want to constrain.

Asking the receptionist for someone's extension is itself a visit in the logbook. Counting how many times you asked tells you about your own questions, not about how busy that person was.

saying these in an interview costs you the question

  • Claiming you cannot verify anything on a deep-stub chain at all
  • Asserting times(1) on an intermediate getter and trusting the number
  • Using verifyNoMoreInteractions on a deep-stubbed parent mock
  • Thinking Mockito verifies the chain as a single atomic interaction
  • Assuming the test's own navigation calls are not recorded on the mock

context