When is @MockitoSpyBean the wrong tool, and what are its limitations?
answer
- Can't stub/verify final or private methods
- Kotlin: members must be open
- Partial mock = real+stub ambiguity
- Full isolation → use @MockitoBean instead
- Prefer fakes/@TestConfiguration; spy sprawl = smell
basics
~20 sAvoid it when you're really testing implementation details, when the collaborator has an easy fake, or when the method you need to control is final/private (Mockito can't intercept those). Prefer real beans, test doubles wired via configuration, or a full @MockitoBean.
solid answer
~40 s@MockitoSpyBean is a targeted tool, not a default. Its limitations: Mockito can't stub or verify final or private methods, so spying them silently does nothing (Kotlin's default-final methods must be open); partial mocks blur real vs. stubbed behavior, making tests brittle and hard to read; heavy use fragments the context cache and slows the suite; and spies encourage asserting on interactions (implementation) rather than outcomes. It's the wrong tool when a lightweight fake or in-memory implementation wired via @TestConfiguration would be clearer, when you truly want full isolation (use @MockitoBean), or when the design pressure it relieves signals a missing seam. Architecturally, reach for it only to observe a hard-to-otherwise-verify real collaboration or to stub one narrowly-scoped slow/nondeterministic method.
code
kotlin · 17 lines// Kotlin gotcha: default-final methods can't be spied.
// Target bean must expose OPEN members for @MockitoSpyBean to work.
@Service
open class ClockService {
open fun now(): Instant = Instant.now() // 'open' required to stub
}
@SpringBootTest
class ReportTest {
@MockitoSpyBean lateinit var clock: ClockService
@Test
fun `stubs one nondeterministic seam, rest stays real`() {
doReturn(Instant.parse("2026-01-01T00:00:00Z")).`when`(clock).now()
// ... real report logic runs against a fixed clock
}
}go deeper
Not expected to reason about when NOT to use it.
Knows the final/private limitation and Kotlin open requirement.
Contrasts spy vs mock vs fake and cites context-cache and interaction-testing concerns.
Sets policy: spies are a last resort for a single real seam; proliferation signals missing abstractions; prefers outcome-based tests and configuration-wired fakes.
At a **principal** level the question isn't *how* to use `@MockitoSpyBean` but *whether* to, and understanding its hard limits. **Technical limitations:** 1. **Final and private methods.** Mockito works by subclassing/instrumenting. It cannot override `private` methods (they aren't polymorphic) and, depending on the mock maker, `final` methods are also not interceptable for verify/stub in the general case. On a spy, calls to such methods go straight to the real implementation and are **invisible** to `verify()` and un-stubbable — a silent no-op that can mislead. In **Kotlin**, classes and methods are `final` by default, so target beans need `open` members (or the kotlin-spring/all-open compiler plugin, which only opens Spring-stereotyped classes) for spying to function. 2. **Self-invocation / proxying.** If the target bean is itself wrapped by a Spring AOP proxy (transactions, caching), the interaction between the Mockito spy and the proxy can be subtle; internal `this.method()` self-calls may bypass the intended interception. 3. **Partial-mock ambiguity.** A spy mixes real and stubbed behavior. Readers must track which methods are overridden; forgetting the `doReturn` idiom triggers real side effects during setup. Mockito's own docs caution that spying is a 'code smell' outside legacy/edge scenarios. 4. **Context-cache fragmentation & cost** (as with any bean override) — different spy sets multiply cached contexts. **When it's the wrong tool:** - *You want full isolation* → use `@MockitoBean` (a complete mock), not a spy. - *An easy fake exists* → wire an in-memory/stub implementation via `@TestConfiguration`/`@Bean`; it's clearer and keeps the context shareable and Mockito-free. - *You're asserting on interactions that are pure implementation detail* → prefer state/outcome-based testing; over-verifying couples tests to internals and makes refactors painful. - *The need for a spy signals a missing abstraction* → the better fix may be to extract the slow/nondeterministic dependency behind an interface you can substitute cleanly. **When it's genuinely right:** - Observing that real code performed a hard-to-otherwise-detect collaboration (e.g. 'the real service published exactly this event') while keeping end-to-end behavior. - Narrowly stubbing *one* real method that is slow, nondeterministic (clock, random, external I/O) or environment-dependent, while everything else stays real — an integration-flavored test with a single seam. **Strategy takeaway:** default to real collaborators and test outcomes; use `@MockitoBean` for isolation; reserve `@MockitoSpyBean` for the narrow 'mostly-real, one seam, want to verify' case, and treat proliferation as a design smell worth refactoring away.
- Your team's spy is on a Kotlin @Service and the stub 'does nothing'. Why?Kotlin classes/methods are final by default and Mockito can't override final members, so the real method runs and the stub is ignored. Make the class/method open (or rely on the kotlin-spring all-open plugin for Spring stereotypes) so the spy can intercept it.
- When would you choose @MockitoBean over @MockitoSpyBean?When you want complete isolation of a collaborator — no real behavior at all — such as replacing an external gateway or a slow/side-effecting dependency entirely. A spy is for when you mostly want real behavior with a narrow override or interaction check.
- Why might heavy spy usage be a design smell?It often means you're reaching into a class to control internals you can't substitute cleanly, indicating a missing seam/abstraction. It also couples tests to implementation and fragments the context cache. Extracting the dependency behind an interface usually yields clearer tests.
saying these in an interview costs you the question
- Using a spy where a full mock (isolation) is actually wanted
- Assuming final/private methods can be stubbed on a spy
- Ignoring Kotlin final-by-default when spying Spring beans
- Treating spies as a default testing approach rather than a narrow tool