When you write a unit test for a service class that has several collaborators, how do you decide which of them to replace with Mockito mocks and which to construct for real?
answer
- real is the default, mock needs a reason
- boundary = I/O, time, randomness
- never mock value objects or collections
- stubbing longer than assertion = smell
- hard to test = too many collaborators
basics
~20 sMock the boundary: collaborators that leave the process or are slow, nondeterministic or hard to set up (repositories, HTTP clients, clocks, message publishers). Construct everything else for real: value objects, DTOs, enums, collections, pure helpers. Real is the default; a mock needs a reason.
solid answer
~50 sMy default is the real object; a mock has to earn its place. It earns one when the collaborator crosses a boundary I do not want inside a unit test: a database repository, an HTTP or messaging client, a mail sender, a clock or random source, a third-party SDK. Those are slow, nondeterministic or need infrastructure, and they are also where the observable outcome lives, so I want to verify the call. Everything fast, deterministic and in-process I build for real: value objects like `Money` or `OrderId`, DTOs, enums, `List`/`Map`, mappers, pure calculators. Mocking those makes the test longer, weaker (I end up asserting my own assumption about `equals`) and blind to real defects. Practical check: if the stubbing block is longer than the assertion, or I am stubbing something with no side effects, I should have used the real class.
code
java · 24 lines@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository repository; // I/O boundary
@Mock PaymentGateway gateway; // remote boundary
OrderService service;
@BeforeEach
void setUp() {
service = new OrderService(repository, gateway, new PricingRules(),
Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC));
}
@Test
void doesNotChargeOrdersOverTheLimit() {
Order order = new Order(new OrderId("A-1"), Money.of("5000.00", "EUR"));
service.place(order);
verify(gateway, never()).charge(any());
verify(repository).save(order);
}
}go deeper
Recall the default: real objects unless the collaborator touches the database, network, files, time or randomness. Give concrete examples on both sides.
Explain why a mocked value object weakens the test (stubbed getters, mock equals/hashCode) and give the check that stubbing longer than the assertion signals overmocking.
Frame it as design feedback: mock count tracks coupling, so push I/O to the edges and keep a pure core needing almost no mocks.
Discuss it as suite-wide policy — where the mocking boundary sits, what that implies for the integration layer, and the long-run cost of tests whose truth is entirely author-supplied.
## The rule in one line Mock the boundary, not the neighbourhood. A unit test replaces the collaborators that make it slow, flaky or impossible to run in isolation, and uses the real class for everything else. Real is the default; every mock must be justified. ## What counts as a boundary A boundary is a collaborator whose real implementation drags something outside your test process into the test, or makes the test unpredictable: - persistence: repositories, DAOs, JDBC/JPA templates; - remote calls: HTTP clients, gRPC stubs, cloud SDKs; - messaging: Kafka or RabbitMQ producers, event publishers; - side-effecting infrastructure: mail senders, file systems, payment gateways; - nondeterminism: clocks, random generators, UUID factories. These are also the collaborators whose *interaction* is the outcome you care about. When the rule is "an order over the limit is not charged", the assertion is that the gateway was never called. A Mockito mock gives you isolation and verification in one move. ## What should stay real Everything fast, in-process and deterministic: - value objects and DTOs (`Money`, `OrderId`, `Address`) — build them with their constructor; - enums and constants — a mocked enum is nonsense; - collections — never mock `List`, `Map` or `Optional`; use `ArrayList`, `Map.of`, `Optional.of`; - pure logic: mappers, validators, calculators, formatters. Mocking a value object means stubbing getters, and stubbing getters means the test asserts what you told the mock to say. Worse, a mocked value object carries a mock's `equals`/`hashCode`, so equality assertions and lookups in maps and sets behave differently from production. Real value objects cost nothing to construct and let the test exercise real behaviour. ## Why the default matters Each mock is a copy of an assumption. A real collaborator keeps its own invariants; a mock keeps yours. The more mocks a test holds, the more of the test's truth is fiction you wrote yourself, and the fewer real defects it can catch. Mock-heavy tests are also setup-heavy: five stubbings, one assertion, and the reader cannot see the scenario for the scaffolding. ## How to decide in practice Ask three questions about the collaborator: 1. Would the real one make the test slow, flaky, or require Docker, network or files? If yes, mock it. 2. Is the *call itself* the behaviour under test — an email sent, an event published? If yes, mock it so you can verify. 3. Otherwise, is it cheap and deterministic? Then use it for real. A design consequence follows: if a class cannot be tested without mocking a dozen things, it has too many collaborators, or business logic is tangled with I/O. Pushing I/O to the edges leaves a core you can test with almost no mocks at all. ## The shape it produces A healthy Mockito test has a small number of mocks at the edges, real domain objects in the middle, one clear act step, assertions on the returned value, and verification of the one or two boundary effects that matter. Such a test is short, survives refactoring, and fails for reasons a reader can act on.
- Why is mocking a value object such as Money considered wrong even when it would shorten the test?A mocked value object only knows what you stubbed, so the test asserts your own assumption rather than the class's behaviour. It also loses real equals and hashCode, which changes assertEquals results and lookups in maps and sets. Value objects are cheap to construct, so the shortcut buys nothing and removes real coverage.
- What do you do when a class needs so many mocks that the setup dwarfs the test?Treat it as a design signal, not a testing problem. Usually the class mixes orchestration with business logic or has grown too many collaborators. Extracting the pure logic into a class with no I/O dependencies makes it testable with no mocks, leaving a thin orchestrator whose few boundary interactions are easy to verify.
A flight simulator replaces the weather and the airport, not the pilot's checklist or the seat belt. You fake what you cannot summon on demand and keep the cheap real parts real.
saying these in an interview costs you the question
- 'Unit test means every dependency is mocked' — the criterion is the boundary, not the dependency count
- Mocking DTOs, enums or value objects and stubbing their getters
- Mocking List, Map or Optional instead of constructing real ones
- Mocking a fast, side-effect-free collaborator purely out of habit
- Claiming heavy mocking is unavoidable instead of reading it as a design smell