skip to content

Mockito's spy() wraps a real object so that some methods run their real implementation while others are stubbed. In test-double terms, what kind of double is that, and what practical hazards come with using one?

level: seniorimportance: should knowfreq 42%

answer

  1. spy = real behaviour + selective overrides = partial double
  2. when(spy.x()) runs the real x() → use doReturn().when(spy).x()
  3. spy(obj) copies fields; original untouched
  4. final/private/static not intercepted by default mock maker
  5. spying the class under test = missing collaborator

basics

~20 s

A spy is a partial double: real behaviour plus selective overrides — closer to a hand-written partial fake than to a pure mock, though it records calls so verify() also works. Hazards: when(spy.x()) actually executes the real method (use doReturn), the spy is a copy of your object, and final or static methods are not intercepted.

solid answer

~50 s

A Mockito spy is a **partial double**. Unlike `mock()`, which replaces every method with a default answer, `spy()` delegates to real implementations except where you override, so it carries real behaviour with holes punched in it — the closest Mockito gets to a partial fake, while still recording invocations so `verify()` works. The hazards are concrete: 1. `when(spy.load()).thenReturn(x)` **calls the real `load()`** while setting up the stub, which may hit a database or throw. Use `doReturn(x).when(spy).load()`. 2. `spy(existingObject)` creates a *copy* with fields shallow-copied; the original is untouched, so anything still holding the original sees nothing. 3. `final`, `private` and `static` methods are not intercepted without the inline mock maker. 4. Spying on the **class under test** to stub away part of it usually means the class does two things; extracting a collaborator is the real fix. Spies are legitimate for legacy code you cannot restructure yet, and for large third-party classes where only one method needs to change.

code

java · 11 lines
java
List<String> list = spy(new ArrayList<String>());

// WRONG: real get(0) executes during stubbing -> IndexOutOfBoundsException
// when(list.get(0)).thenReturn("x");

// RIGHT: the real method is never called
doReturn("x").when(list).get(0);

list.add("real");            // real behaviour still runs
assertEquals("x", list.get(0));
verify(list).add("real");    // spies record invocations too

go deeper

for a junior

Know that a spy runs real code except where stubbed, and that doReturn().when(spy) is the safe stubbing form.

for a middle

Explain the partial-double nature, the copy semantics of spy(obj), and the interception limits.

for a senior

Diagnose why a stub appears ignored, and argue when a spy is a legitimate legacy compromise versus a signal to extract a collaborator.

for a principal

Frame spies as a boundary-blurring tool: acceptable as scaffolding around code you intend to refactor, unacceptable as a standing pattern, with review guidance that caps how much of an object may be stubbed away.

## Where a spy sits Mockito builds two kinds of object. `mock(Type.class)` intercepts every method and never runs real code. `spy(instance)` (or `@Spy`) also intercepts every method, but the default behaviour is to call the real implementation. You then override individual methods. That makes a spy a *partial* double: mostly real behaviour, with selected responses replaced. It is not a fake in the strict sense — a fake is a working alternative implementation you write and own, whereas a spy is the production implementation with selective holes. It is not a pure mock either, because most calls do real work. It does record every invocation, so `verify()` works on a spy exactly as on a mock, which is where the name comes from: it watches a real object. ## Hazard 1: stubbing executes the real method ```java List<String> list = spy(new ArrayList<>()); when(list.get(0)).thenReturn("x"); // IndexOutOfBoundsException: real get(0) runs doReturn("x").when(list).get(0); // correct form ``` `when(spy.method())` evaluates its argument, which means the real method is invoked before Mockito can intercept the stubbing. On a mock that is harmless (the call returns a default); on a spy it can throw, hit I/O, or mutate state. The `doReturn/doThrow/doAnswer/doNothing().when(spy).method()` family avoids calling the method at all. This is the single most common spy bug, and knowing the fix is table stakes for the question. ## Hazard 2: the spy is a copy `Mockito.spy(realObject)` does not decorate the instance you passed. It creates a new proxy instance and shallow-copies the fields across. Consequences: - Mutating the spy does not mutate the original, and vice versa; a test that configures the original after spying sees nothing. - Anything that captured a reference to the original — a registry, a listener list, a Spring bean already injected elsewhere — keeps talking to the un-spied object, so your stubbing appears to have no effect. - Shallow copying means mutable fields are shared, which can produce surprising cross-talk. The safe pattern is to spy first and inject the spy everywhere, rather than spying on an object that is already wired in. ## Hazard 3: interception limits With the default (subclass) mock maker, spies cannot intercept `final` classes, `final` methods, `private` methods or `static` methods, and calls made from the real code into such methods simply run for real. `mockito-inline` (the default mock maker in recent Mockito versions) lifts the final restrictions via instrumentation, but private and static calls remain special cases — static requires `mockStatic`. When a spy "ignores" your stubbing, this is the second thing to check after the copy problem. Note that self-calls *are* intercepted for the methods Mockito can override: real code executing on the spy runs with `this` bound to the proxy, so an internal call to an overridden method hits your stub. That is precisely how people stub away one step of a template method — and precisely why it is fragile, because it depends on an internal call path that refactoring may change. ## Hazard 4: spying on the class under test The pattern `OrderService service = spy(new OrderService(...)); doReturn(fakePrice).when(service).calculatePrice(any());` tests a mutant: the object under test is no longer the object that ships. It hides a design signal — if a class has a part you need to neutralise to test the rest, that part is a separate responsibility. Extracting it into a collaborator lets you inject an ordinary stub and test the real class. A related smell is `verify(spyUnderTest).someInternalMethod()`, which asserts on the class's own internals and locks the implementation in place. ## When spies are the right call - **Legacy code** you cannot yet restructure: a large class with one method that must be neutralised (a `sleep`, an outbound call, a clock read) so the rest can be exercised. A spy buys you a characterisation test, which then funds the refactoring. - **Third-party or framework classes** where writing a full double means implementing dozens of methods and you only need to change one. - **Verifying calls on a real object** whose behaviour you deliberately want to keep — for example checking that a real collection was actually written to, while it behaves normally. In all three, the honest framing in an interview is: a spy is a controlled compromise. It keeps real behaviour, which is good for fidelity, and it blurs the boundary of the system under test, which is bad for clarity. Reach for it deliberately, keep the number of stubbed methods small, and treat a growing set of overrides as a signal to extract a collaborator.

  • A colleague stubs a spy with when(spy.loadFromDisk()).thenReturn(data) and the test fails with an IOException during setup. What is happening and what is the fix?
    The argument of when(...) is evaluated before Mockito can intercept it, so the real loadFromDisk() executes and throws while the stub is being declared. The fix is the do-family form, doReturn(data).when(spy).loadFromDisk(), which routes the stubbing through the proxy without invoking the real method. The same applies to doThrow, doNothing and doAnswer on spies.
  • You stub a method on a spy and the production code seems to ignore the stub entirely. What are the two most likely reasons?
    Either the code is still holding the original instance rather than the spy — Mockito.spy copies the object, so anything wired up before the spy was created keeps using the original — or the method cannot be intercepted, typically because it is final, private or static under the mock maker in use. Checking which instance is actually injected, and the method's modifiers, resolves nearly all such cases.

saying these in an interview costs you the question

  • Using when(spy.method()) on a spy without realising the real method runs
  • Believing spy(obj) decorates the original instance
  • Expecting stubs on final, private or static methods to take effect by default
  • Routinely spying the class under test to stub away inconvenient parts
  • Calling a spy a full fake, as though it were a hand-written working implementation

context