A teammate stubs a mock repository with a thenAnswer that keeps an internal HashMap of saved entities and serves findById from it. What are the risks of this style of stubbing, and when would you push back?
answer
- Stateful Answer = untested second implementation
- Leaks: static/shared map, order-dependent failures
- Mockito does not lock your answer body
- Behaviour buried in arrange, traces through internals
- Promote to a named fake, or delegatesTo(fake) to keep verify
basics
~20 sAn Answer holding state is a second, untested implementation of the collaborator hidden in test setup. Risks: the test validates code against that stand-in, state leaks between tests, and the Answer is not thread-safe unless you make it so. If it needs state, write an explicit fake.
solid answer
~60 sA stateful `Answer` stops being a stub and becomes an implementation. Concerns, in order: 1. **It is untested code.** Nothing verifies the map-backed repository behaves like the real one - it silently ignores constraints, uniqueness, flush semantics. Tests can be green against a collaborator that does not exist in production. 2. **Leakage.** If the `Answer` (or the map it closes over) is a field on the test class or, worse, static, entities survive into later tests. Order-dependent failures follow. 3. **Concurrency.** Mockito does not synchronise your answer body. If the code under test calls the mock from multiple threads, a plain `HashMap` or `int++` inside the answer is a data race. 4. **Readability and debugging.** Behaviour now lives in setup, far from the assertions, and stack traces run through Mockito internals. When the collaborator genuinely needs memory, promote it to a named hand-written fake (or use the real class with an in-memory backing) that lives in test fixtures, is readable, and can itself be tested. Keep `Answer` for one-liners: echo an argument, invoke a callback, throw for a specific input.
code
java · 11 lines// Fine: bounded, obvious, local to one retry test
AtomicInteger attempts = new AtomicInteger();
when(client.call()).thenAnswer(inv -> {
if (attempts.incrementAndGet() < 3) throw new IOException("flaky");
return "ok";
});
// Better than a map inside a lambda: a named fake, still verifiable
UserRepository fake = new InMemoryUserRepository();
UserRepository repo = mock(UserRepository.class,
withSettings().defaultAnswer(AdditionalAnswers.delegatesTo(fake)));go deeper
Say that big lambdas in test setup are hard to read and that state left over between tests causes failures that depend on order.
Name the concrete risks - leakage, no thread safety, logic hidden in arrange - and propose splitting the test or using fixed stubs instead.
Frame it as an untested second implementation, cover the concurrency and reset points, and offer delegatesTo(fake) as the migration that preserves verification.
Argue the suite-wide policy: where behaviour is allowed to live in tests, when a fake earns a name and its own tests, and what the maintenance cost of behaviour-in-mocks is over years.
## What has actually happened A stub answers a question; an implementation makes decisions. The moment an `Answer` closes over a `Map` and serves reads from what earlier writes put there, it has crossed from the first category into the second. That is not automatically wrong - it is sometimes the pragmatic choice - but it changes what the test proves and what it costs to maintain, and a senior candidate should be able to say why. ## Risk 1: an untested second implementation The map-backed repository is production-shaped logic with no tests of its own. Real repositories enforce things the map does not: uniqueness constraints, generated ids, cascading, lazy loading, flush timing, an exception on a missing row rather than a null. Code that passes against the map can fail in production the first time any of those matter, and the test suite is confidently green throughout. The suite has begun verifying the code under test against a fiction that the same author wrote in the same sitting - a tautology, not a check. ## Risk 2: state leaking between tests Where does the map live? If it is a field on the test class initialised at declaration, JUnit 5's default per-method test instance saves you; if it is `static`, or if the mock is created once in a `@BeforeAll` or shared through a Spring test context, entries survive across tests. The symptom is the worst kind of failure: passes alone, fails in the suite, fails differently after a reorder. Any stateful answer must have an obvious, per-test reset point. ## Risk 3: concurrency Mockito records invocations for its own bookkeeping, but it does **not** wrap your answer body in a lock. If the code under test hits the mock from an executor, a parallel stream, or a `@Async` path, two threads run the answer at once. `HashMap` under concurrent mutation can corrupt or spin; `count++` loses updates. Either use concurrent structures inside the answer or accept that the mock is single-threaded only. Note that Mockito's own stubbing API is also not designed for stubbing from multiple threads while calls are in flight - stub before you start the threads. ## Risk 4: readability and diagnosis A test reads best as arrange / act / assert. A large answer buries behaviour in arrange, so a reader chasing an unexpected value must first reconstruct a mini-implementation. Exceptions thrown from inside answers surface with stack frames through Mockito internals, and a debugger stepping into the mock lands in generated code before reaching your lambda. Small answers avoid all of this; large ones pay it repeatedly. ## Risk 5: it hides a design signal When a test needs a collaborator with memory, that is information. Often the unit under test is doing too much, or the collaborator's interface is too chatty, or the test is really an integration test wearing a unit-test costume. Reaching for a stateful mock silences the signal instead of listening to it. ## What to do instead - **Hand-written fake.** A small `InMemoryUserRepository implements UserRepository` in test fixtures: named, readable, reusable, and testable in its own right. If verification is still wanted, wrap it with `mock(UserRepository.class, withSettings().defaultAnswer(delegatesTo(fake)))` and keep `verify()`. - **Real implementation with in-memory wiring** when one exists and is cheap. - **Narrower tests.** Frequently the map exists only because one test exercises save-then-read. Splitting into two tests with plain fixed stubs removes the need for state entirely. ## Where stateful answers are still fine - A counter that makes a retry test terminate: fail twice, then succeed. That is small, obvious, and local. - Recording the argument of an asynchronous callback so the test can complete it later - though an `ArgumentCaptor` usually reads better. - A sequence of responses, which `thenAnswer(returnsElementsOf(...))` expresses without hand-rolled state. ## The pushback, phrased well "If the collaborator needs memory for this test to make sense, let's give it a name. An `InMemoryUserRepository` in fixtures is the same amount of code, it is readable in one place, we can reset it per test, and if it ever drifts from the real repository we can test it. A lambda in setup gets us the same behaviour with none of those properties."
- How would you keep verify() on interactions while still getting real, stateful behaviour from a fake?Create the mock with delegatesTo(fake) as its default answer. Calls route through the mock - so they are recorded and verifiable - and then land on the hand-written fake, which supplies the behaviour. You get interaction assertions and realistic responses without writing behaviour into an Answer lambda.
- Your stateful Answer works alone but the test fails when the class is run with other tests. What do you look at first?Where the state lives and when it is created. A static field, a mock created in @BeforeAll, or a mock shared through a cached Spring context all keep entries across tests. Move creation into @BeforeEach (or reset the state there), and confirm the failure disappears when the tests are run in the failing order.
It is the difference between a stand-in reading one line and a stand-in improvising a whole scene: the second one is doing the actor's job, and nobody has reviewed the script.
saying these in an interview costs you the question
- Assuming Mockito synchronises Answer execution, so no thread-safety is needed
- Treating a map-backed answer as equivalent to the real repository's semantics
- Keeping the state in a static field and calling the resulting order dependence flakiness
- Using an Answer to record arguments for later assertions where an ArgumentCaptor is clearer
- Insisting all stateful answers are wrong - a bounded retry counter is perfectly reasonable