skip to content

How do you use @MockitoSpyBean to verify a collaboration while keeping real behavior?

level: middleimportance: should knowfreq 55%

answer

  1. Spy runs real code, records calls
  2. verify(spy).method(args)
  3. times()/never()/atLeastOnce()
  4. ArgumentCaptor for exact args
  5. Kotlin methods must be open to spy

basics

~10 s

Annotate the collaborator field with @MockitoSpyBean, autowire the service under test, exercise it, then call Mockito verify(spy).method(args) to assert the real bean was invoked as expected — the real logic still runs.

solid answer

~40 s

Because a spy wraps the real bean and runs real code by default, it's ideal for verifying interactions without changing behavior. Put @MockitoSpyBean on the collaborator, @Autowired the class under test (which receives the spy through DI), call the entry-point method, then use verify(spy) with argument matchers, times()/never()/atLeastOnce(), and optionally ArgumentCaptor to assert what was passed. Nothing is stubbed, so side effects and return values are genuine — you're only observing. This is the spy's sweet spot: assert 'the service actually called the repository/publisher with these arguments' while integration-style behavior remains intact. Keep in mind verification counts include real calls made during the test, so reset() or careful ordering matters if the bean is shared.

code

java · 15 lines
java
@SpringBootTest
class RegistrationServiceTest {

    @MockitoSpyBean EmailNotifier notifier; // real bean, spied
    @Autowired RegistrationService service;

    @Test
    void sendsWelcomeEmailOnRegister() {
        service.register("[email protected]"); // real notifier logic runs

        ArgumentCaptor<String> to = ArgumentCaptor.forClass(String.class);
        verify(notifier, times(1)).sendWelcome(to.capture());
        assertThat(to.getValue()).isEqualTo("[email protected]");
    }
}

go deeper

for a junior

Knows verify(spy).method() confirms a call happened.

for a middle

Must combine real behavior + verify + matchers/captor and know no stubbing needed.

for a senior

Discusses final/Kotlin-open constraints and invocation-count pollution on shared beans.

for a principal

Weighs interaction testing vs. state testing and warns against over-verifying implementation details.

**Verifying collaborations** means asserting that the code under test *interacted* with a dependency in the expected way — which method it called, how many times, and with what arguments — rather than only checking the final return value. This is *interaction testing*, and **@MockitoSpyBean** is well suited to it because it lets the real code run while still recording every call. **Mechanism.** The spy is a real Spring bean placed in the `ApplicationContext`, so any bean that depends on that type is injected with the spy via normal dependency injection. When the service under test calls `collaborator.doThing(x)`, Mockito records the invocation *and* the real `doThing` executes. Afterward you assert with: - `verify(spy).method(arg)` — called exactly once (the default). - `verify(spy, times(2)).method(...)`, `never()`, `atLeastOnce()`, `atMost(n)` — count qualifiers. - Argument matchers: `eq(...)`, `any()`, `anyString()`, custom `argThat(...)`. - `ArgumentCaptor<T>` to capture and then assert on the exact object passed. - `verifyNoMoreInteractions(spy)` / `verifyNoInteractions(spy)` for exhaustiveness. **Why a spy and not a mock here?** With a mock you'd have to stub every method the collaborator needs to return something sensible, otherwise downstream logic breaks on the null/default returns. A spy needs no stubbing — the real logic supplies real results — so you get an integration-flavored test *plus* interaction assertions. **Gotchas:** - *Call counting includes real invocations:* if the spy bean is reused or the framework itself calls it, verification counts reflect that. Use `Mockito.reset(spy)` (sparingly), `clearInvocations(spy)`, or scope the test tightly. - *Final/private methods:* Mockito cannot verify (or stub) `final` or `private` methods on a spy — the invocation goes straight through and is invisible to `verify`. In Kotlin, methods are `final` by default, so target beans often need `open` methods (or the all-open/Spring plugin) for spying to work. - *Self-invocation:* if a bean calls its own method internally (`this.other()`), that call bypasses the spy proxy in some proxying setups and won't be recorded — same caveat as with Spring AOP self-invocation, though a Mockito spy wraps the instance directly so direct spies do capture self-calls, whereas Spring-proxy-wrapped beans may not. **Typical shape:** arrange (nothing to stub), act (call the public entry point), assert (`verify` the collaboration + optionally assert the real result).

  • Can you verify a final or private method with an @MockitoSpyBean?
    No. Mockito's default (inline) mock maker can spy many cases but cannot intercept private methods, and final methods historically can't be verified/stubbed either. The real method runs and no invocation is recorded, so verify() won't see it. In Kotlin you must make the method open.
  • How would you assert a method was NOT called?
    Use verify(spy, never()).method(...) for a specific call, or verifyNoInteractions(spy) / verifyNoMoreInteractions(spy) to assert the bean was untouched or fully accounted for.

saying these in an interview costs you the question

  • Believing you must stub methods on a spy before real behavior works
  • Assuming verify() sees private/final method calls
  • Thinking a mock is better for interaction tests that also need real return values

context