skip to content

Mockito has no API that produces a working in-memory implementation of a dependency — every double it builds is a proxy driven by recorded answers. When a test needs a dependency that stays consistent across many calls, such as an in-memory store, how do you get one, and what changes compared with chains of when(...).thenReturn(...)?

level: seniorimportance: should knowfreq 34%

answer

  1. Mockito answers per call; no memory, no fake factory
  2. thenReturn(a,b,c) = call order, not state
  3. symptom: long when() chains, verify used as state
  4. hand-written map-backed implementation in test sources
  5. contract test keeps the in-memory version honest

basics

~20 s

You write it yourself — a small class implementing the interface over a HashMap — or use a ready-made in-memory tool. Mockito can only return canned answers per call; a hand-written implementation keeps state, so a save followed by a lookup works without scripting every interaction.

solid answer

~60 s

Mockito's whole model is per-invocation answers: matcher matches, answer fires. It has no factory for a working implementation, so if the test needs a dependency whose behaviour is *consistent* — save then read back, delete then miss — you either script every call or step outside Mockito. The practical options: 1. **Hand-write it.** A class implementing the port over a `ConcurrentHashMap`, roughly 30 lines, living in test sources next to the port it implements. 2. **Use an existing in-memory implementation:** an embedded database, an in-memory filesystem, a local HTTP stub server. 3. **`thenAnswer` with a map** — a lambda reading and writing a captured `HashMap`. This works, but you have hand-written an implementation and hidden it inside a stub; if it exceeds a couple of lines, promote it to a real class. What changes: assertions become state-based ("the record is there"), setup shrinks dramatically for multi-step flows, and tests survive refactoring. The cost is code you own and must keep faithful — which is what a contract test against the real implementation is for.

code

java · 14 lines
java
// scripted: every step of the conversation is pinned
when(repo.findById(1L)).thenReturn(Optional.empty(), Optional.of(alice));
when(repo.save(any())).thenReturn(alice);
service.register("alice");
verify(repo).save(argThat(u -> u.name().equals("alice")));

// with memory: arrange once, assert on state
InMemoryUserRepository repo = new InMemoryUserRepository();
UserService service = new UserService(repo);

service.register("alice");
service.deactivate(repo.findByName("alice").orElseThrow().id());

assertFalse(repo.findByName("alice").orElseThrow().isActive());

go deeper

for a junior

Say that Mockito returns canned answers per call and that anything needing memory is written as a small class over a map.

for a middle

Recognise the symptoms — long when() chains, order-dependent thenReturn, verify standing in for state — and describe the switch to state-based assertions.

for a senior

Argue the trade-off explicitly: less coupling and shorter setup versus code you own, mitigated by a shared contract test, and name where per-call stubbing still wins.

for a principal

Turn it into policy: which ports get a maintained in-memory implementation, who owns the contract tests, and how that choice shapes the suite's resistance to refactoring.

## The boundary in Mockito's model Mockito builds proxies. Each intercepted call is matched against recorded stubbings, and the matching `Answer` produces a value. That model is excellent for "when asked X, reply Y" and structurally poor at "remember what you were told". `thenReturn(a, b, c)` returns values in *call order*, not according to state; there is no `Mockito.fake(...)`, and no way to declare that `save` should influence a later `findById`. So the question is not really about Mockito syntax. It is about recognising the point where per-call answers stop being the right shape and something with memory is needed. ## The symptom that tells you Watch for these in a test: - Six or more `when(...)` lines before a single action, most of them wiring up one flow. - Stubbing the same method with different values for successive calls to simulate a changing world. - Tests that break whenever the production code reorders calls or adds a lookup — a sure sign the test encodes the call sequence rather than the outcome. - `verify` used as a proxy for state, because there is no state to assert: `verify(repo).save(argThat(u -> u.isActive()))` instead of reading the record back. When a flow spans several interactions with the same dependency, per-call answers force you to script a whole conversation, and every scripted line is a coupling point. ## What you do instead **1. Write the implementation.** For a narrow, test-owned port this is small: ```java class InMemoryUserRepository implements UserRepository { private final Map<Long, User> store = new ConcurrentHashMap<>(); public User save(User u) { store.put(u.id(), u); return u; } public Optional<User> findById(Long id) { return Optional.ofNullable(store.get(id)); } public void delete(Long id) { store.remove(id); } } ``` It lives in test sources, is shared by every test of that area, and pays for itself the second time it is used. Keep it dumb: a map and no clever behaviour. The moment it grows conditionals that mirror production logic, it has become a second implementation to maintain and its value evaporates. **2. Reuse an existing one.** An embedded/in-memory database, an in-memory filesystem, a local stub HTTP server, an in-memory message channel. Someone else maintains the fidelity, which removes the main cost. **3. `thenAnswer` over a captured map.** Legitimate for a one-off: ```java Map<Long, User> store = new HashMap<>(); when(repo.save(any())).thenAnswer(inv -> { User u = inv.getArgument(0); store.put(u.id(), u); return u; }); when(repo.findById(any())).thenAnswer(inv -> Optional.ofNullable(store.get(inv.getArgument(0)))); ``` This *is* a hand-written implementation wearing a proxy's clothes: the Mockito API adds nothing except an awkward place to put the code. Beyond one or two methods, extract the class. It also loses type safety on arguments and produces worse stack traces on failure. ## What changes in the test **Assertions move from interaction to state.** Instead of `verify(repo).save(argThat(...))` you write `assertTrue(repo.findById(1L).orElseThrow().isActive())`. That reads closer to the requirement and does not break when the code saves via a different method. **Setup collapses.** A multi-step flow — register, then activate, then look up — needs one line of arrangement instead of a script per step. **Refactoring stops breaking tests.** Because nothing pins the call sequence, changing how the code reaches the outcome is free as long as the outcome holds. That is the largest long-run benefit. **You take on maintenance.** The implementation must stay faithful enough that passing tests mean something. The standard mitigation is a shared contract test: one abstract test class asserting the port's behaviour, run against both the in-memory implementation and the real one (the real run tagged as integration). When they diverge, you learn immediately rather than in production. ## When Mockito is still the right answer Do not over-rotate. Per-call stubbing remains ideal when the dependency answers a single question (`RateProvider.rateFor`), when you need an error path (`thenThrow`), when the collaborator is a command whose invocation is the assertion (`EmailSender.send` plus `verify`), or when the interface is wide and only one method matters — writing 40 empty methods to stub one is a bad trade, and that is exactly what Mockito's proxy generation saves you. The judgement being probed is: can you name the boundary of your tool, and choose deliberately on either side of it rather than bending one tool to cover both cases?

  • What stops a hand-written in-memory implementation from drifting away from the real dependency's behaviour?
    A shared contract test: one abstract test class that asserts the port's required behaviour, extended once per implementation, with the real one tagged so it runs in the slower integration job. Both must pass the same assertions, so a divergence such as case-sensitive lookups or different null handling shows up as a failing test rather than as a production surprise. Without that, the in-memory version quietly becomes a fiction that keeps tests green.
  • You see a Mockito stub whose thenAnswer lambda reads and writes a captured HashMap. What would you say about it?
    It works, but the code inside the lambda is a hand-written implementation stored in an awkward place: untyped arguments, no name, no reuse, and poor failure diagnostics. If it is one method for one test I would leave it; beyond that I would extract a small class implementing the interface, which is shorter to read, reusable across the test class, and can be covered by a contract test.

A stub is a card with one rehearsed line; an in-memory implementation is an understudy who has learned the part. For a single cue the card is faster; for a whole scene you want someone who remembers what happened in the previous act.

saying these in an interview costs you the question

  • Believing Mockito can generate a stateful working implementation for you
  • Using thenReturn(a, b, c) to model state instead of call order
  • Growing an in-memory implementation until it reimplements production logic
  • Writing an in-memory implementation with no contract test to keep it honest
  • Abandoning Mockito entirely and hand-writing doubles for wide interfaces where only one method matters

context