Spies
A mock is an empty stand-in: every method returns an empty value until you stub it. A spy is the opposite: it wraps a real object, all methods work as usual, but Mockito records the calls and lets you replace individual methods. Spies are needed rarely — when you want to keep the real behavior and only check or change a small part of it.
A spy on a real repository
// src/test/java/com/example/blog/SpyTest.java
package com.example.blog;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class SpyTest {
@Test
void spyCallsRealMethods() {
InMemoryPostRepository repository = spy(new InMemoryPostRepository());
PostService service = new PostService(repository, mock(Notifier.class));
Post post = service.create("Hello", "anna");
assertEquals(1L, post.id());
assertEquals(1, repository.findAll().size());
verify(repository).save(any(Post.class));
}
}spy(...) takes a ready object. The real save() assigns the id 1 and stores the post — findAll() really returns one post. At the same time verify works as with a mock.
Replacing one method
// src/test/java/com/example/blog/SpyTest.java
@Test
void spyCanOverrideOneMethod() {
InMemoryPostRepository repository = spy(new InMemoryPostRepository());
doReturn(Optional.of(new Post(7L, "From spy", "oleg", false)))
.when(repository).findById(7L);
PostService service = new PostService(repository, mock(Notifier.class));
assertEquals("From spy", service.find(7L).title());
}findById(7L) now returns the prepared post, all other methods remain real.
Mock or spy
| Mock | Spy | |
|---|---|---|
| Created from | A class or interface | A ready object |
| Unstubbed methods | Return empty values | Call the real code |
| Stubbing | when(...).thenReturn(...) | doReturn(...).when(spy).method(...) |
| When to use | Almost always | Legacy code, checking calls on a real object |
If you want a spy on the class under test itself — for example, to replace one of its own methods — it is usually a sign that the class does too much. Moving that part into a separate dependency and mocking it gives a cleaner design.
Common mistake
With a spy, when(spy.method()) first calls the real method — that is how Java evaluates the argument of when. If the real method throws, the stubbing never happens:
PostService service = spy(new PostService(new InMemoryPostRepository(), (to, message) -> { }));
when(service.find(7L)).thenReturn(new Post(7L, "From spy", "oleg", false));[ERROR] com.example.blog.SpyWhenTest.spyWithWhen -- Time elapsed: 1.618 s <<< ERROR!
com.example.blog.PostNotFoundException: Post not found: 7
at com.example.blog.PostService.find(PostService.java:24)
at com.example.blog.SpyWhenTest.spyWithWhen(SpyWhenTest.java:16)The real find(7L) looked into an empty repository and threw an exception. For spies always use the doReturn(...).when(spy).method(...) form: it does not call the real method.