When would you reach for a spy over a mock, and what design risks does heavy spy usage signal?
answer
- Mock by default, spy by exception
- Spy use: legacy / template hook / verify real object
- Risk: couples to internals, real I/O, self-call bypasses stub
- Many spies = SRP/DI smell
- Fix: inject + decompose, then mocks suffice
basics
~20 sUse a mock by default to fully isolate a dependency. Reach for a spy only when you need most of an object's real behavior but want to override or verify a small part. Lots of spies usually means a class is hard to test and probably doing too much.
solid answer
~50 sDefault to mocks: they isolate the class under test completely and force you to be explicit about which interactions matter, giving clearer, more robust tests. Reach for a spy only in narrow cases: you have a real object whose behavior you mostly want, but you need to stub one expensive or non-deterministic method, or verify a self-managed call — common with legacy classes you can't easily refactor, or template-method-style classes where you test the orchestration while stubbing one hook. The risks: spies run real code, so tests become coupled to real implementation details and can do real I/O or mutate state unexpectedly; partial mocks blur the unit boundary; and self-calls inside the spy bypass your stubs, producing surprising behavior. Heavy reliance on spies is a design smell — it usually signals a class with mixed responsibilities or hidden collaborators that should be extracted into separately mockable dependencies. The cleaner long-term fix is refactoring toward dependency injection and small, single-responsibility classes, after which mocks suffice.
go deeper
Knows you can use a spy when you want real behavior plus a small override, without nuance on risks.
Picks mock-by-default and can name a legacy/template case for a spy; aware spies run real code.
Weighs the coupling/real-I/O/self-call risks, justifies spy use case-by-case, and prefers refactoring toward injectable collaborators.
Treats spy prevalence as a testability/design metric, sets team conventions, and drives decomposition + DI so mocks suffice; precise about Mockito vs textbook double semantics.
## Start from the default: prefer mocks A **mock** replaces a collaborator entirely (no real code), so the test under it is fully **isolated** and **deterministic**. This is the desired default because it (a) forces you to declare exactly which interactions matter, (b) cannot accidentally perform real I/O, and (c) keeps tests decoupled from the collaborator's internals. If you can express your test with a mock, do. ## Legitimate reasons to choose a spy A **spy** wraps a **real object**, runs real methods by default, and lets you override/verify a slice (**partial mocking**). Genuine use cases: 1. **Legacy code you can't refactor yet.** A large class with intertwined logic and one expensive method (say, a remote call). A spy lets you keep the real logic but stub the one method, as a *bridge* while you work toward a cleaner design. 2. **Template-method / skeleton classes.** A class defines an algorithm calling overridable hooks. You may want the real orchestration but a stubbed hook — though even here, extracting the hook into an injected strategy is usually better. 3. **Verifying real-object interactions.** Occasionally you want a real object's behavior *and* to assert a specific internal method was invoked a certain number of times. 4. **Third-party classes** you can't change and that lack clean seams — a spy can be a pragmatic wrapper. ## The risks that make spies a smell - **Coupling to implementation.** Because real methods run, the test now depends on the collaborator's *internal* behavior, not just its contract. Refactors that preserve the contract can still break spy-based tests. - **Accidental real work.** Real methods can do I/O, mutate shared state, or be non-deterministic — sometimes silently during setup (the `when(spy.foo())` side-effect trap). - **Self-call blind spot.** When a real spy method calls `this.other()`, that internal call runs **real** code, *not* your stub — the stub only intercepts calls through the spy reference. People expect their partial mock to apply transitively; it doesn't. - **Blurred unit boundary.** A partial mock is half real, half fake; it's harder to reason about "what am I actually testing here?" ## Reading it as a design signal If a codebase leans heavily on spies, it usually means classes are **doing too much** (violating single-responsibility) or have **hidden, non-injected collaborators** (e.g. `new`-ing dependencies internally, calling static factories, reaching into singletons). Those are exactly the things that make a class impossible to mock cleanly — so people reach for spies as a workaround. The principal-level move is to treat the spy count as a **testability metric**: a rising one points at code that needs **dependency injection** and **decomposition** into small, single-responsibility units, after which ordinary mocks are sufficient and tests get clearer. ## Setting team conventions A reasonable policy: mocks by default; spies allowed only with a comment explaining why the real behavior is needed and a note on the refactor that would remove it; never spy a class purely to avoid wiring a constructor dependency. Pair this with strict stubbing (Mockito's `MockitoExtension` default) so unused/over-stubbing is caught, keeping partial mocks honest. ## Where this fits historically The broader xUnit double taxonomy (dummy/fake/stub/spy/mock) names a *spy* as "a stub that also records calls." Mockito's `spy()` is more aggressive — it records **and** runs real code by default — so the library term and the textbook term overlap but aren't identical. In interviews, be precise about *Mockito's* spy semantics rather than reciting the taxonomy.
- A teammate spies a service just to avoid constructing one of its dependencies. What do you advise?Don't — that's using a spy to mask poor injection. Make the dependency a constructor parameter and mock it. Spying to dodge wiring couples the test to real internals and hides the design problem.
- Why doesn't stubbing methodB on a spy affect methodA when methodA calls this.methodB internally?The stub only intercepts calls made through the spy reference. A real method's internal this.methodB() call runs the real method directly, bypassing the proxy, so the stub doesn't apply.
saying these in an interview costs you the question
- Treating spies as interchangeable with mocks and using them by default.
- Spying to avoid injecting a dependency rather than fixing the design.
- Assuming a stub on a spy applies to internal self-calls.
- Ignoring that spy-based tests couple to the collaborator's real implementation, not just its contract.