skip to content

How do you create a Mockito spy, and what are the ways to do partial mocking on it?

level: middleimportance: should knowfreq 60%

answer

  1. Mockito.spy(realObject) or @Spy
  2. @Spy needs MockitoExtension / openMocks
  3. No-arg ctor used if no instance given
  4. doReturn/doNothing/doThrow for safe spy stubbing
  5. Spy copies state — use the spy, not the original

basics

~10 s

Create a spy with Mockito.spy(realObject) or the @Spy annotation (with @ExtendWith(MockitoExtension.class) or initMocks). Partial mocking means the spy runs real methods except the few you stub.

solid answer

~40 s

There are two main ways to make a spy. Programmatically: Spy<List> spy = Mockito.spy(new ArrayList<>()) — you pass a real instance to wrap. Declaratively: annotate a field with @Spy and enable Mockito (JUnit 5 @ExtendWith(MockitoExtension.class), or MockitoAnnotations.openMocks(this) in setup). With @Spy you can either provide an initialized instance (@Spy List<String> list = new ArrayList<>()) or let Mockito instantiate it via the no-arg constructor. Partial mocking is the spy's whole point: by default the real methods run, and you override only selected ones. To stub safely use doReturn(x).when(spy).method() or doNothing()/doThrow() for void methods, since when(spy.method()).thenReturn(x) would invoke the real method first. You can mix real and stubbed behavior freely; calls to non-stubbed methods still execute real code and are recorded for verification.

code

java · 17 lines
java
@ExtendWith(MockitoExtension.class)
class DiscountTest {

    @Spy
    PriceCalculator calc = new PriceCalculator();   // real instance wrapped

    @Test
    void appliesStubbedTaxButRealRounding() {
        // partial mock: override tax(), keep real round()
        doReturn(0.0).when(calc).tax(anyDouble());   // safe stubbing

        double total = calc.total(100.0);            // real total() runs,
                                                     // calls stubbed tax() + real round()
        assertEquals(100.0, total);
        verify(calc).round(anyDouble());             // real call still recorded
    }
}

go deeper

for a junior

Knows Mockito.spy(obj) and @Spy exist and that a spy wraps a real object. May not know the annotation wiring.

for a middle

Creates spies both ways, wires MockitoExtension/openMocks, and uses doReturn().when() for partial mocking of selected methods.

for a senior

Explains the state-copy semantics of spy(real), the no-arg-constructor requirement of @Spy, and the self-call caveat where internal calls bypass stubs.

for a principal

Guides when partial mocking is acceptable vs a refactor signal, sets test conventions, and reasons about how spy semantics interact with @InjectMocks and constructor-injected designs.

## Goal: wrap a real object, override a little A **spy** is a test double that holds a **real instance** and runs its real methods by default, while letting you **override (stub) a few** and **verify** all calls. Overriding only part of an object's behavior is **partial mocking**. This section is the practical "how do I make one" companion to the conceptual mock-vs-spy distinction. ## Way 1 — programmatic: Mockito.spy(...) ```java import static org.mockito.Mockito.*; List<String> real = new ArrayList<>(); List<String> spy = Mockito.spy(real); ``` You must give `spy(...)` a **real object** (or a class — `spy(ArrayList.class)` instantiates one). Internally Mockito creates a proxy that **copies the state** of the passed object and delegates method calls to real implementations. Note it operates on a **copy**: mutating the original `real` afterward will not be seen by the spy, and vice versa. Always interact with the **spy**, not the original. ## Way 2 — declarative: @Spy ```java @ExtendWith(MockitoExtension.class) // JUnit 5 class OrderServiceTest { @Spy List<String> names = new ArrayList<>(); // explicit instance @Spy OrderValidator validator; // Mockito builds via no-arg ctor } ``` `@Spy` needs Mockito's annotation processing turned on: - **JUnit 5:** `@ExtendWith(MockitoExtension.class)` on the test class. - **JUnit 4:** `@RunWith(MockitoJUnitRunner.class)` or `MockitoAnnotations.openMocks(this)` in an `@Before` method (older code: `initMocks`). If you don't assign an instance, Mockito constructs one using the type's **no-argument constructor**; if there isn't one, it fails — so provide an instance for types without a no-arg constructor. `@Spy` also composes with `@InjectMocks`: a spied field can be injected into the class under test alongside `@Mock`s. ## Partial mocking — stubbing the slice you care about By default, calling any spy method runs **real code**. To override one: ```java doReturn("stubbed").when(spy).get(0); // safe: real get(0) is NOT called spy.add("x"); // real add runs String s = spy.get(0); // returns "stubbed" ``` For `void` methods that you don't want to actually execute: ```java doNothing().when(spy).expensiveLog(); // suppress the real void method doThrow(new RuntimeException()).when(spy).risky(); ``` Why the `do*().when(spy)` form? With `when(spy.get(0)).thenReturn("x")`, Java **evaluates `spy.get(0)` first**, invoking the **real** method (which on a fresh `ArrayList` throws `IndexOutOfBoundsException`). The `do*` family hands `when` the spy **without** calling the target method, so it's the safe idiom for spies and for any method with side effects. ## Verifying Everything you'd do with a mock works on a spy: ```java verify(spy).add("x"); verify(spy, times(2)).get(anyInt()); ``` Real calls are recorded too, so verification covers both real and stubbed invocations. ## Practical cautions - A spy's real methods can call **other real methods on the same object**; those internal calls also run real code, *not* your stubs (the stub only intercepts calls **through the spy reference**). This surprises people doing partial mocking of self-calling methods. - Heavy partial mocking is a **design smell** — it often means the class mixes responsibilities you can't easily isolate. Prefer extracting collaborators you can mock cleanly.

  • You wrote @Spy on a field of a class with no no-arg constructor and didn't assign an instance. What happens?
    Mockito fails to create the spy because it tries to instantiate via the no-arg constructor. Fix it by assigning a constructed instance: @Spy Foo foo = new Foo(arg);.
  • Inside a spy, methodA() calls this.methodB(). You stubbed methodB(). Does the stub apply during methodA()'s internal call?
    No — the internal this.methodB() call runs the real method, because the stub only intercepts calls made through the spy reference, not self-calls inside the real object.

saying these in an interview costs you the question

  • Passing a class with no no-arg constructor to @Spy without an instance and expecting it to work.
  • Mutating the original object after spy(real) and expecting the spy to reflect it — the spy works on a copy.
  • Forgetting @ExtendWith(MockitoExtension.class) / openMocks, so @Spy fields stay null.
  • Using when().thenReturn() to stub a spy method that has side effects or throws.

context