Across a large test suite, when would you invest in writing and maintaining a hand-rolled in-memory implementation of a collaborator such as a repository, instead of configuring Mockito mocks in each test that needs it?
answer
- repetition, statefulness, leaking rules
- mock = cheap per test, costly per suite
- fidelity risk: no constraints, no transactions
- one contract suite run against both
- keep interaction assertions on mocks
basics
~20 sInvest when the same collaborator is stubbed in dozens of tests, when scenarios need consistent behaviour across calls (save then read back), or when stubbings keep encoding rules. A shared in-memory implementation removes duplicated setup and holds invariants once; the cost is maintaining it and defending its fidelity.
solid answer
~50 sI look at three signals. Repetition: if twenty tests each stub `findById`, `save` and `existsBy...` the same way, that is duplicated setup which will drift. Statefulness: when a scenario needs save-then-read semantics or uniqueness, a mock forces you to hand-script every interaction, while an in-memory map implementation simply behaves. Rule leakage: if stubbings keep encoding logic ("returns empty when soft-deleted"), the assumption now lives in many files instead of one. Against that I weigh the costs. A hand-written implementation must be maintained as the interface grows, and it can quietly diverge from the real store — no constraint violations, no ordering, no transactional rollback — producing a confidence a mock at least makes visibly artificial. Mitigations: keep the interface narrow, run one contract test suite against both implementations, and keep real integration tests for persistence semantics. Mocks remain the better tool for one-off edge cases and for asserting that a call happened.
go deeper
Keep it simple: mocks are fine for a few tests; when the same setup is copied everywhere or a test needs save-then-read behaviour, a shared in-memory class is easier.
Give the concrete triggers (repeated stubbing, stateful scenarios) and name the fidelity gap between an in-memory store and a real database.
Discuss migration strategy and the contract-test technique that keeps both implementations honest, plus which collaborators should stay mocked.
Frame it as suite economics and risk placement: where fidelity must be real, what the maintained substitute costs over years, and how the choice shapes the layers of the test pyramid.
## The decision, framed Both options replace a collaborator: a Mockito mock configured per test, or a small hand-written class implementing the same interface over an in-memory data structure. This is an investment question — the second carries up-front and ongoing cost that pays back only at a certain scale of use. ## Signals favouring the hand-written implementation **Repetition across the suite.** When the same three or four stubbings appear in dozens of test classes, that setup is shared code that happens to be copied. Any change to the interface, or to what "not found" means, forces a sweep across all of them. One implementation centralises it. **Stateful scenarios.** Mockito is call-oriented: each stubbing answers one call. Tests needing continuity — save then load, insert twice and expect a uniqueness failure, delete then verify absence, paginate over what was written — must script every step and quickly become unreadable. An in-memory map gives continuity for free, and the test reads as a story about behaviour rather than about calls. **Rules leaking into stubbings.** If every test must remember "returns empty for soft-deleted rows", that rule now lives in fifty places. A single implementation holds it once, and changing it updates the whole suite. **Readability of higher-level tests.** Tests exercising several services together become nearly unwritable with mocks, because each service's calls must be anticipated. A shared in-memory implementation lets them state a scenario and assert an outcome. ## Signals favouring mocks **Low usage.** A collaborator touched by three tests does not justify a maintained class. **Interaction is the assertion.** When the point is "the gateway was charged exactly once" or "no mail was sent", a mock gives verification directly; a hand-written stand-in would need its own call recording, which is re-implementing Mockito. **One-off edge cases.** Timeouts, exotic exceptions, a call returning garbage: stubbing one call is far cheaper than teaching an implementation to misbehave on demand. **Wide or unstable interfaces.** Implementing a thirty-method interface, or one still in flux, is a maintenance tax — and a hint the interface should be split. ## The real risk: divergence The strongest argument against a hand-written stand-in is fidelity. An in-memory repository has no unique constraints, no cascades, no lazy loading, no transaction rollback, no collation rules and no optimistic-locking conflicts. A suite running entirely on it can be uniformly green while the real database rejects half the writes — and unlike a mock, its plausibility invites over-trust. Three mitigations. Keep the interface narrow and domain-shaped, so there is little semantics to reproduce. Write one contract test suite — behavioural tests written against the interface — and run it against both the in-memory and the real implementation, so drift fails a test rather than production. And keep a thin layer of real integration tests for the persistence-specific behaviour that no in-process substitute can honestly reproduce. ## Phasing the investment Start with mocks. When a collaborator crosses roughly a dozen test classes, or the first stateful scenario becomes painful, extract the implementation and migrate opportunistically: new tests use it, old ones move when touched. Treat it as production-quality code — reviewed, small, owned by whoever owns the interface. If it starts growing conditionals to satisfy individual tests, that is the signal it has become a mock with worse ergonomics and should be split or retired. ## What the answer should convey There is no universal winner. Mocks are cheap per test and expensive per suite; a maintained in-memory implementation is expensive once and cheap thereafter, provided its fidelity is actively defended. The judgement turns on scale of reuse, statefulness of the scenarios, and how much of the real collaborator's semantics the tests genuinely depend on.
- How do you stop a hand-written in-memory implementation from drifting away from the real one?Write the behavioural expectations once as a contract test suite against the interface and run it against both implementations — the in-memory one in the fast suite, the real one in the integration suite. Drift then shows up as a failing test rather than a production incident, and new interface methods force both sides to be updated.
- Which collaborators would you never replace with an in-memory implementation?Ones where the interaction itself is the assertion — payment gateways, mail senders, event publishers — since you would re-implement the call recording Mockito already provides. Also collaborators with wide or unstable interfaces, where maintenance outweighs reuse, and those used by only a handful of tests.
saying these in an interview costs you the question
- Claiming hand-written in-memory implementations are always better than mocks
- Assuming an in-memory map proves the real database will accept the writes
- Letting the stand-in accumulate per-test conditionals until it is an awkward mock
- Dropping all real persistence integration tests once the in-memory version exists
- Building one for a collaborator used in two tests