Command Palette

Search for a command to run...

[Advanced Java] Mocking with Mockito: Stubbing, Verification and When Not to Mock

A unit test wants to run one class in isolation. That class usually has collaborators — a repository that talks to a database, a mail sender that opens a socket, a clock that returns a different answer every second. You cannot assert anything stable while those are in the picture, and a test that needs a running database is not a unit test.

A test double is any object you put in a collaborator's place so the test can control it. Mockito is the library most Java projects use to build one, but the important part is not the library. It is knowing which double you need, what a mock actually records, and when the whole approach starts producing tests that pass while the code is broken.

A unit under test wired to either a real collaborator or a mock through the same seam

Everything below — every program output, every exception, every failure report — was compiled and executed on OpenJDK 21.0.6 (arm64) with Mockito 5.14.2, JUnit Jupiter 5.11.3 on JUnit Platform 1.11.3, Byte Buddy 1.15.4 and Objenesis 3.3 on the classpath.

Why a test double exists at all

Here is the code under test for the whole article. A service, two collaborators behind interfaces, and a record.

Java
public record User(long id, String email, boolean active) {}
 
public interface UserRepository {
    User findById(long id);
    void save(User user);
}
 
public interface EmailSender {
    boolean send(String to, String subject);
}
 
public class AccountService {
    private final UserRepository repo;
    private final EmailSender email;
 
    public AccountService(UserRepository repo, EmailSender email) {
        this.repo = repo;
        this.email = email;
    }
 
    public String deactivate(long id) {
        User u = repo.findById(id);
        if (u == null) return "not found";
        repo.save(new User(u.id(), u.email(), false));
        email.send(u.email(), "Account deactivated");
        return "ok";
    }
}

AccountService never mentions a database or an SMTP server. It depends on two interfaces, and that is the seam: anything implementing UserRepository can be handed to the constructor. In production it is a JDBC implementation; in a test it is whatever you want.

The same unit under test wired to a JDBC repository in production and to a mock in a test

The first thing to notice is that you do not need a library to exploit that seam. A double is just a class.

Java
@Test
void aFakeIsJustAClass() {
    UserRepository fake = new UserRepository() {
        public User findById(long id) { return new User(id, "ann@example.com", true); }
        public void save(User user) { }
    };
    EmailSender fakeSender = (to, subject) -> true;
 
    assertEquals("ok", new AccountService(fake, fakeSender).deactivate(1L));
}

That passes, with no dependency beyond JUnit. EmailSender has a single abstract method so its double is one lambda. Mockito exists because writing that by hand stops being pleasant once the interface has fifteen methods, once you want a different answer per argument, and once you want to assert that a method was called. It is a convenience, not a requirement — and reaching for a hand-written fake first is often the better instinct, particularly for an in-memory repository that several tests share.

The Mockito version of exactly the same test:

Java
@Test
void theSameTestWithMockito() {
    UserRepository repo = mock(UserRepository.class);
    EmailSender email = mock(EmailSender.class);
    when(repo.findById(1L)).thenReturn(new User(1L, "ann@example.com", true));
 
    assertEquals("ok", new AccountService(repo, email).deactivate(1L));
}

Dummy, stub, spy, mock, fake — they are not synonyms

Almost everyone says "mock" for all five. The distinctions come from Gerard Meszaros and they are worth knowing, because they describe what a double is for:

DoubleWhat it doesTypical use
DummyGets passed around and never usedFilling a parameter you do not care about
StubReturns canned answers to callsFeeding the unit under test its input
SpyA real object that also records what happened to itKeeping real behaviour, overriding one method
MockA stub whose expected calls are part of the assertionAsserting that a side effect happened
FakeA working implementation, simplifiedIn-memory repository, in-memory clock

Mockito builds all of these from one mock() call. An object created with mock() is a stub while you are calling when(...) on it and a mock once you verify(...) it — the difference is what you do, not what you made. spy() is the only one with its own factory method, and Mockito.mock(Foo.class) passed as an argument you never touch is a dummy.

What a mock returns before you stub anything

Every method on a fresh mock has an answer already. This is the single largest source of confusing test failures, so it is worth seeing directly rather than guessing:

Java
interface Probe {
    String text();
    int count();
    long total();
    double rate();
    boolean enabled();
    List<String> items();
    Map<String, Integer> index();
    Optional<String> maybe();
    User user();
    int[] array();
}
 
@Test
void whatAnUnstubbedMockReturns() {
    Probe p = mock(Probe.class);
    System.out.println("text()    -> " + p.text());
    System.out.println("count()   -> " + p.count());
    System.out.println("total()   -> " + p.total());
    System.out.println("rate()    -> " + p.rate());
    System.out.println("enabled() -> " + p.enabled());
    System.out.println("items()   -> " + p.items() + "  size=" + p.items().size());
    System.out.println("index()   -> " + p.index());
    System.out.println("maybe()   -> " + p.maybe());
    System.out.println("user()    -> " + p.user());
    System.out.println("array()   -> " + Arrays.toString(p.array()));
}
Text
text()    -> null
count()   -> 0
total()   -> 0
rate()    -> 0.0
enabled() -> false
items()   -> []  size=0
index()   -> {}
maybe()   -> Optional.empty
user()    -> null
array()   -> null

So: primitives get their zero value, String and any other reference type get null, and collection-shaped return types get an empty instance rather than null. Optional returns Optional.empty — not null, which is the one case people are most often wrong about. Arrays are the exception in the other direction: int[] comes back null, not an empty array.

The null is what bites. Take the same service written without the null check:

Java
static class NaiveAccountService {
    private final UserRepository repo;
    NaiveAccountService(UserRepository repo) { this.repo = repo; }
    String emailOf(long id) { return repo.findById(id).email().toLowerCase(); }
}
 
@Test
void forgettingToStubGivesYouAnNpe() {
    UserRepository repo = mock(UserRepository.class);
    NullPointerException e = assertThrows(NullPointerException.class,
            () -> new NaiveAccountService(repo).emailOf(1L));
    System.out.println("message: " + e.getMessage());
}
Text
message: Cannot invoke "User.email()" because the return value of "UserRepository.findById(long)" is null

Nothing is broken in the service. The test simply forgot to stub findById, and the mock did what it always does. Helpful NullPointerException messages, on by default since Java 15, name the exact call — read them before assuming the production code is wrong.

Stubbing: programming the answers

when(call).thenReturn(value) records an answer for a call pattern. The call inside when(...) is a real invocation on the mock; Mockito intercepts it and treats it as the pattern rather than as a call to record.

Java
@Test
void basicStubbing() {
    UserRepository repo = mock(UserRepository.class);
    when(repo.findById(1L)).thenReturn(new User(1L, "ann@example.com", true));
 
    assertEquals("ann@example.com", repo.findById(1L).email());
    assertNull(repo.findById(2L), "an argument you did not stub falls back to the default");
}

An argument you did not stub is not an error — it falls back to the default from the previous section. thenThrow covers the failure path, and consecutive calls to the same stub chain answers in order, with the last one repeating forever:

Java
when(repo.findById(1L))
        .thenThrow(new IllegalStateException("timeout"))
        .thenReturn(new User(1L, "ann@example.com", true));
Text
call 1 -> java.lang.IllegalStateException: timeout
call 2 -> User[id=1, email=ann@example.com, active=true]
call 3 -> User[id=1, email=ann@example.com, active=true]

A void method cannot be wrapped in when(...), because when needs a return value to attach to. Use the do* family, which puts the mock first:

Java
doThrow(new IllegalArgumentException("read-only")).when(repo).save(any());
Text
doThrow -> java.lang.IllegalArgumentException: read-only

Two rules that are easy to miss. Stubbing the same call twice replaces the answer, and when two different patterns both match a call, the last one declared wins. And thenReturn evaluates its argument once, at stubbing time — if you want a fresh value per call, use thenAnswer.

Argument matchers, and the rule everyone breaks once

A raw value in a stub means "exactly this argument". A matcher widens it:

Java
EmailSender email = mock(EmailSender.class);
when(email.send(anyString(), eq("Welcome"))).thenReturn(true);
when(email.send(argThat(to -> to != null && to.endsWith("@blocked.test")), anyString()))
        .thenReturn(false);
Text
ann@example.com / Welcome  -> true
ann@example.com / Receipt  -> false
bob@blocked.test / Welcome -> false

The third line is the last-declared-wins rule in action: both stubs match it, and the later one answers. The second line matched no stub at all, so it fell through to the boolean default.

MatcherMatches
any()Any value including null
anyString(), anyInt(), anyLong()Any non-null value of that type
eq(v)A value equal to v, used to mix a literal in among matchers
argThat(pred)Anything the predicate accepts
isNull(), isNotNull()Exactly what they say
same(v)The same reference, not merely equal

Now the rule. Matchers are not values — each call to any() pushes an entry onto a thread-local stack, and Mockito pops one per argument when the stubbed call returns. Mix a literal in among them and the counts do not line up:

Java
when(email.send("ann@example.com", anyString())).thenReturn(true);
Text
org.mockito.exceptions.misusing.InvalidUseOfMatchersException
 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at MatchersTest.mixingRawValuesAndMatchersFails(MatchersTest.java:24)
 
This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));
 
For more info see javadoc for Matchers class.

The fix is in the message: wrap the literal in eq(...). It is either all matchers or no matchers, never a mixture. The stack is thread-local, which is also why a stray matcher left behind by one test can surface as a bizarre failure in the next one.

Verification: asserting on the calls that went out

Stubbing controls what flows into the unit under test. Verification asserts on what flowed out of it — the calls it made on its collaborators. Those are two independent halves of the same mock, and keeping them straight is most of what makes mock-based tests readable.

A mock as an answer table written by stubbing and a call log read by verification

Java
@Test
void verifyTheCallsThatMatter() {
    UserRepository repo = mock(UserRepository.class);
    EmailSender email = mock(EmailSender.class);
    when(repo.findById(1L)).thenReturn(new User(1L, "ann@example.com", true));
 
    new AccountService(repo, email).deactivate(1L);
 
    verify(repo).findById(1L);
    verify(repo, times(1)).save(any(User.class));
    verify(email).send("ann@example.com", "Account deactivated");
    verifyNoMoreInteractions(repo, email);
}

verify(mock) with no mode means times(1). The other modes are never(), times(n), atLeastOnce(), atLeast(n), atMost(n) and only(). Verifying that something did not happen is often the more valuable assertion:

Java
String result = new AccountService(repo, email).deactivate(404L);
 
assertEquals("not found", result);
verify(repo, never()).save(any());
verify(email, never()).send(anyString(), anyString());
verify(repo, atLeastOnce()).findById(anyLong());

A failed verification is an AssertionError, and the report tells you what it wanted and what it got:

Text
org.mockito.exceptions.verification.TooFewActualInvocations
 
emailSender.send(<any string>, <any string>);
Wanted 2 times:
-> at VerifyTest.whatAFailedVerifyLooksLike(VerifyTest.java:74)
But was 1 time:
-> at AccountService.deactivate(AccountService.java:14)

verifyNoMoreInteractions asserts that every interaction on the mock has already been verified. When it fails it prints the whole call log, marking unverified entries with a question mark:

Text
No interactions wanted here:
-> at VerifyTest.whatVerifyNoMoreInteractionsLooksLikeWhenItFails(VerifyTest.java:93)
But found this interaction on mock 'userRepository':
-> at AccountService.deactivate(AccountService.java:13)
***
For your reference, here is the list of all invocations ([?] - means unverified).
1. -> at AccountService.deactivate(AccountService.java:11)
2. [?]-> at AccountService.deactivate(AccountService.java:13)

That is a useful diagnostic and a dangerous assertion — more on why in the pitfalls section.

InOrder verifies relative ordering across one or several mocks:

Java
InOrder inOrder = inOrder(repo, email);
inOrder.verify(repo).save(any(User.class));
inOrder.verify(email).send(anyString(), anyString());

And ArgumentCaptor answers the question verify cannot: not "was it called" but "what was it called with". This is the tool that turns an interaction test back into a real assertion.

Java
ArgumentCaptor<User> saved = ArgumentCaptor.forClass(User.class);
verify(repo).save(saved.capture());
System.out.println("captured: " + saved.getValue());
assertFalse(saved.getValue().active());
Text
captured: User[id=1, email=ann@example.com, active=false]

getValue() returns the last captured argument; getAllValues() returns every one, in call order. Capture in the verification phase, not the stubbing phase: a captor is itself a matcher, so when(repo.findById(captor.capture())) widens the stub to match every argument and then fills the captor with the values seen during stubbing rather than the ones you meant to assert on.

@Mock, @InjectMocks and MockitoExtension

MockitoExtension creates the annotated mocks before each test and validates usage afterwards. It lives in the separate mockito-junit-jupiter artifact.

Java
@ExtendWith(MockitoExtension.class)
class AnnotationsTest {
 
    @Mock UserRepository repo;
    @Mock EmailSender email;
    @InjectMocks AccountService service;
 
    @Test
    void annotationsWireTheServiceForYou() {
        when(repo.findById(1L)).thenReturn(new User(1L, "ann@example.com", true));
 
        assertEquals("ok", service.deactivate(1L));
        verify(email).send("ann@example.com", "Account deactivated");
    }
}

The extension also turns on strict stubs, which fails a test that declares a stub nobody calls. That sounds pedantic until you realise an unused stub usually means the test is not exercising the path its author thought it was:

Text
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at StrictStubsTest.aStubNobodyCalls(StrictStubsTest.java:14)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

@InjectMocks is the part to be careful with. It picks the biggest constructor it can satisfy and fills what it can; a parameter with no matching mock is passed as null, silently. Add a third collaborator to the constructor and forget to add the matching @Mock, and nothing complains at wiring time:

Java
public AccountServiceV2(UserRepository repo, EmailSender email, AuditLog audit) { ... }
Java
@Mock UserRepository repo;
@Mock EmailSender email;          // no @Mock AuditLog
@InjectMocks AccountServiceV2 service;
Text
NPE from @InjectMocks gap: Cannot invoke "AuditLog.record(String)" because "this.audit" is null

The field was constructed, the test started, and the failure arrived deep inside the service as a NullPointerException with nothing pointing at the real cause. Calling the constructor yourself costs one line and cannot fail this way:

Java
@Mock UserRepository repo;
@Mock EmailSender email;
@Mock AuditLog audit;
 
private AccountServiceV2 service;
 
@BeforeEach
void setUp() { service = new AccountServiceV2(repo, email, audit); }

A missing collaborator is now a compile error. That is the whole argument for constructor injection by hand.

Spy: a real object with a few methods replaced

spy(obj) wraps a real instance. Unstubbed methods run the real code; stubbed ones do not.

Java
static class RateTable {
    private final Map<String, Double> rates = new HashMap<>(Map.of("EUR", 1.08));
 
    double rate(String currency) {
        System.out.println("  [real rate() ran for " + currency + "]");
        Double r = rates.get(currency);
        if (r == null) throw new NoSuchElementException("no rate for " + currency);
        return r;
    }
 
    double convert(double amount, String currency) { return amount * rate(currency); }
}
Java
RateTable spy = spy(new RateTable());
assertEquals(108.0, spy.convert(100, "EUR"), 1e-9);   // real code, real answer

Now the trap. when(spy.rate("JPY")) has to evaluate spy.rate("JPY") before when ever sees it — and on a spy, that evaluation runs the real method:

Java
RateTable spy = spy(new RateTable());
when(spy.rate("JPY")).thenReturn(0.0065);
Text
--- when(spy.rate("JPY")) ---
  [real rate() ran for JPY]
  threw: java.util.NoSuchElementException: no rate for JPY

The real method ran and threw before stubbing happened. On a plain mock this never shows up, because the real method is not there to run. On a spy it always does, and it is why the do* forms exist — they name the mock first and never evaluate the call:

Java
RateTable spy = spy(new RateTable());
doReturn(0.0065).when(spy).rate("JPY");
assertEquals(65.0, spy.convert(10000, "JPY"), 1e-9);
Text
--- doReturn().when(spy).rate("JPY") ---
  convert -> 65.0

No real-method line in the output: rate("JPY") was never really called. Use doReturn().when(spy).method() on spies as a habit, not as a fallback. The same applies to any method whose real execution would be slow, destructive, or throw.

⚠️ A spy wraps a copy of the object you pass in. Mockito creates a new instance and copies the fields across, so changes made through the spy are not visible on the original reference, and stubbing that depends on identity will surprise you.

Mocking final classes and static methods

This is where the answer depends on the version, so check rather than remember. Mockito picks a MockMaker at startup, and you can print which one it chose:

Java
MockMaker maker = org.mockito.internal.configuration.plugins.Plugins.getMockMaker();
System.out.println("mock maker: " + maker.getClass().getName());

On Mockito 5.14.2 with nothing configured:

Text
mock maker: org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker

The inline mock maker is the default in Mockito 5. It instruments loaded classes through the JVM instrumentation API instead of generating a subclass, so final classes, final methods and static methods are all mockable out of the box:

Java
static final class Tokenizer {
    String tokenize(String card) { return "real-" + card; }
}
 
static class Ids {
    static String next() { return "real-id"; }
}
Java
Tokenizer t = mock(Tokenizer.class);
when(t.tokenize(anyString())).thenReturn("tok_123");
System.out.println("final class mock -> " + t.tokenize("4111"));
 
System.out.println("before scope: " + Ids.next());
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
    ids.when(Ids::next).thenReturn("id_42");
    System.out.println("inside scope: " + Ids.next());
}
System.out.println("after scope:  " + Ids.next());
Text
final class mock -> tok_123
before scope: real-id
inside scope: id_42
after scope:  real-id

mockStatic returns a scoped resource and it must be closed. The replacement is registered per thread, so skipping the try-with-resources leaves it active and the next test that mocks the same class in that thread fails with a message that says so:

Text
org.mockito.exceptions.base.MockitoException
 
For FinalAndStaticTest$Ids, static mocking is already registered in the current thread
 
To create a new mock, the existing static mock registration must be deregistered

On Mockito 2, 3 and 4 the default was the subclass mock maker, which cannot do any of this. That configuration is still selectable, by putting a file named mockito-extensions/org.mockito.plugins.MockMaker containing mock-maker-subclass on the classpath, and it is worth seeing what it reports, because this is the error message thousands of Stack Overflow answers are about:

Text
mock maker: org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker
 
Cannot mock/spy class SubclassMakerProbe$Tokenizer
Mockito cannot mock/spy because :
 - final class
 
The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks
 
Mockito's inline mock maker supports static mocks based on the Instrumentation API.
You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
Note that Mockito's inline mock maker is not supported on Android.

If you hit that message on a modern project, the cause is almost always an old Mockito rather than anything you did. On Mockito 5 the separate mockito-inline artifact the message mentions is no longer needed.

There is one honest wrinkle with the inline maker on a modern JDK. It needs a Java agent, and by default it attaches one to its own JVM at runtime, which the JDK now warns about:

Text
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build what is described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (byte-buddy-agent-1.15.4.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: Dynamic loading of agents will be disallowed by default in a future release

Nothing is broken — the tests pass — but self-attachment is on its way out, and the warning is telling you to declare the agent up front instead. mockito-core ships a Premain-Class, so adding it as an agent silences both warnings:

Bash
java -javaagent:mockito-core-5.14.2.jar \
     -cp junit-platform-console-standalone-1.11.3.jar:out:mockito-core-5.14.2.jar:byte-buddy-1.15.4.jar:byte-buddy-agent-1.15.4.jar:objenesis-3.3.jar \
     org.junit.platform.console.ConsoleLauncher execute --select-class=FinalAndStaticTest

Running that produced the same passing results with no self-attach message and no JDK agent warning. Assembling the classpath by hand like this is only for a minimal setup; a build tool normally does it for you.

The pitfalls that make a mock-heavy test worthless

Everything above is mechanics. This section is the part that decides whether the tests you write are worth their maintenance cost.

Two shapes of the same test: a mock at every seam versus one mock at the boundary

A verification is an assertion about your implementation. Take a service that happens to call its repository twice:

Java
public String greet(long id) {
    if (repo.findById(id) == null) return "unknown";
    return "Hello " + repo.findById(id).email();
}

An over-specified test pins that down:

Java
new GreetingService(repo).greet(1L);
verify(repo, times(2)).findById(1L);

Now do the obvious cleanup — hold the result in a local instead of looking it up twice:

Java
public String greet(long id) {
    User u = repo.findById(id);
    if (u == null) return "unknown";
    return "Hello " + u.email();
}

The observable behaviour is byte-for-byte identical. A test asserting on the returned string passes against both versions. The interaction test does this:

Text
org.mockito.exceptions.verification.TooFewActualInvocations
 
userRepository.findById(1L);
Wanted 2 times:
-> at OverMockingTest.interactionTestBreaksAfterTheRewrite(OverMockingTest.java:31)
But was 1 time:
-> at GreetingServiceRefactored.greet(GreetingServiceRefactored.java:7)

Red on a refactor that fixed nothing and broke nothing. Multiply that by a suite of a few thousand and you have a test suite that punishes cleanup, which is exactly the behaviour you least want to discourage. InOrder and verifyNoMoreInteractions are the two sharpest tools for building tests like this, so use them where the ordering or the absence of extra calls is genuinely part of the contract, and nowhere else.

And the stronger point: a test that mocks everything tests nothing. Here is a service with a real bug — it saves the user without ever clearing the active flag:

Java
public String deactivate(long id) {
    User u = repo.findById(id);
    if (u == null) return "not found";
    repo.save(new User(u.id(), u.email(), true));   // bug: should be false
    email.send(u.email(), "Account deactivated");
    return "ok";
}

And here is a test of the kind that gets written when mocks are the whole toolbox:

Java
new BuggyAccountService(repo, email).deactivate(1L);
 
verify(repo).findById(1L);
verify(repo).save(any(User.class));
verify(email).send(anyString(), anyString());

It passes. Green, against a deactivation feature that does not deactivate anyone. Every stub returned what the test author wrote, every verification confirmed a call the test author already knew about, and no line of it ever looked at the data. The only assertion left standing is "my code called the methods I told it to call".

One captor turns it back into a real test:

Java
ArgumentCaptor<User> saved = ArgumentCaptor.forClass(User.class);
verify(repo).save(saved.capture());
assertFalse(saved.getValue().active());
Text
state assertion: expected: <false> but was: <true>  saved=User[id=1, email=ann@example.com, active=true]

The rule that falls out of this: assert on state and returned values wherever you can, and use verification only for side effects that leave no observable state behind — a mail actually sent, a message actually published, a row actually deleted. When you do verify, capture the arguments.

When not to mock

Some things should never get a double, and the reason is always the same: the double is more work than the real object and less trustworthy.

Value objects. A record, a String, a LocalDate, a BigDecimal. Constructing one is free and it has no behaviour to isolate you from. Mockito 5 will happily mock a record, and the result is nonsense:

Java
User u = mock(User.class);
System.out.println("mocked record: id=" + u.id() + " email=" + u.email() + " active=" + u.active());
System.out.println("toString      -> " + u.toString());
Text
mocked record: id=0 email=null active=false
toString      -> Mock for User, hashCode: 1471633238

Every accessor returns a default, toString no longer describes the value, and the assertion failure you eventually read tells you nothing. Write new User(1L, "ann@example.com", true).

Collections. A mocked List is not a list — it is an object that has forgotten how to be one:

Java
List<String> list = mock(List.class);
list.add("a");
list.add("b");
System.out.println("mocked list size after two adds -> " + list.size());
System.out.println("mocked list get(0)             -> " + list.get(0));
Text
mocked list size after two adds -> 0
mocked list get(0)             -> null

The add calls were recorded and discarded. Use new ArrayList<>().

The class under test. If you find yourself mocking a method on the object you are testing — which requires a spy — the class is doing two things and wants splitting.

Anything cheap that you own. An in-memory implementation of your own repository interface is often clearer than five lines of stubbing, is shared across the whole test class, and keeps real behaviour like "saving then loading returns what you saved" that a stub cannot express.

The boundary worth mocking is the one you do not control and cannot make fast or deterministic: the network, the filesystem, the clock, a third-party client, a payment provider. Inside that boundary, prefer real objects.

FAQ

Do I need Mockito to write a unit test?

No. A test double is any object that stands in for a collaborator, and for an interface with one or two methods a lambda or a small anonymous class does the job with no dependency at all — the hand-written fake at the top of this article passes on JUnit alone. Mockito earns its place on wide interfaces, when the answer has to vary by argument, and when you need to assert that a call happened. Reaching for a hand-written fake first is a reasonable default, especially for an in-memory repository shared across a test class, where the fake keeps behaviour that stubbing cannot express.

What does a mock return if I forget to stub a method?

A default, never an error. Primitives get their zero value, so int is 0 and boolean is false; reference types get null; collection-shaped returns get an empty instance, and Optional gets Optional.empty rather than null. Arrays are the odd one out and come back null. That null for reference types is why an unstubbed mock so often surfaces as a NullPointerException somewhere inside the class under test rather than as an obvious "you forgot to stub this". Java's helpful NullPointerException message names the call whose return value was null, which usually identifies the missing stub immediately.

Why does when(spy.method()) call the real method?

Because Java evaluates arguments before calling a method. when(spy.rate("JPY")) must produce a value to hand to when, so spy.rate("JPY") runs first, and on a spy the unstubbed real implementation is what runs. If that method throws, hits a database, or deletes something, the damage is done before Mockito is involved. Use doReturn(value).when(spy).rate("JPY") instead: that form names the mock first and never evaluates the call. On a plain mock() the problem is invisible because there is no real implementation behind the call, which is why the trap only ever bites people the first time they use a spy.

Can Mockito mock final classes and static methods?

On Mockito 5 yes, with no configuration — the inline mock maker is the default, and on 5.14.2 Plugins.getMockMaker() reports InlineByteBuddyMockMaker. It instruments loaded classes rather than subclassing them, so final classes, final methods and static methods all work; mockStatic returns a scoped resource that must be closed, or the next test that mocks the same class on that thread fails with "static mocking is already registered in the current thread". On Mockito 2 to 4 the default was the subclass mock maker, which fails with "Mockito cannot mock/spy because : - final class" and refuses static mocks entirely; that is what the old mockito-inline artifact was for. Being able to mock a static method is not the same as it being a good idea — a static dependency you can pass in as a parameter is easier to test than one you instrument away.

What is the difference between a stub and a mock?

A stub feeds input to the class under test; a mock is a double whose calls are themselves part of what you assert. In Mockito both come from mock() and the difference is only in how you use it — when(...) makes it a stub, verify(...) makes it a mock. The distinction still matters when deciding what to write, because a stub failing means the code asked for something you did not prepare, while a mock failing means the code did not do something you demanded. Over-using the second kind is what produces tests coupled to implementation details.

Should I use @InjectMocks?

It is convenient and it fails quietly. @InjectMocks picks the largest constructor it can satisfy and passes null for any parameter it has no mock for, so adding a collaborator to the constructor without adding the matching @Mock produces a NullPointerException deep inside the class under test rather than an error at wiring time. Calling the constructor yourself in a @BeforeEach costs one line and turns that same mistake into a compile error. Keep @Mock for creating the doubles, since that part is unambiguous.

How many mocks is too many in one test?

There is no threshold, but four or five mocks around one class is a strong signal — usually that the class has too many collaborators, or that the boundary was drawn in the wrong place. The more useful question is what the test still proves once every collaborator is fake: if every answer was written by the test and every assertion is a verify, the test can only confirm that your code called the methods you told it to call, which is what the buggy deactivation example above demonstrates. Mock at the edge of your system, keep real objects inside it, and assert on state and returned values wherever they exist.

Conclusion

Mockito is small once the model is clear: a mock is an answer table that when(...) writes into, plus a call log that verify(...) reads back. Stubbing controls what goes in, verification asserts what came out, and ArgumentCaptor is what turns the second one from "it was called" into a real assertion about the data.

The mechanics that catch people are all consequences of that model. An unstubbed method answers with a default, so a forgotten stub arrives as a NullPointerException. Matchers live on a stack, so mixing one with a literal throws InvalidUseOfMatchersException. A spy runs real code, so when(spy.foo()) executes foo() before stubbing it, and doReturn().when(spy).foo() is the form that does not. On Mockito 5 the inline mock maker is the default, so final classes and static methods are mockable, at the cost of a self-attaching agent that a modern JDK warns about.

The judgement calls matter more than any of it. Verify only side effects that leave no state to assert on; assert on values everywhere else. Never pin down a call count or an ordering unless it is genuinely part of the contract, because that is how a suite ends up failing on refactors that changed nothing. And keep the boundary in mind: doubles belong where the network, the clock and the filesystem start, not between two of your own classes that were cheap to construct in the first place.

Part 6 continues with article 30, on debugging and logging in Java — reading a stack trace properly, and getting useful information out of a running program instead of guessing at it.

Related Posts

[Advanced Java] Working with JSON and XML in Java: Gson, Jackson and the Defaults That Bite

JSON binding in Java with Gson 2.10.1 and Jackson 2.17.3: round trips, nested objects and collections, renaming and ignoring fields, the null-handling and unknown-key defaults that differ, TypeToken and TypeReference for generics, java.time support, streaming large documents with JsonReader and JsonParser, the traps that fail somewhere other than the mistake, and what XML looks like on a JDK that no longer ships JAXB.

[Advanced Java] Spring Boot Basics: the IoC Container, Auto-Configuration and the Bean Lifecycle

Spring Framework versus Spring Boot on Spring Boot 4.1.1 and OpenJDK 21: the IoC container and constructor injection, @Primary and @Qualifier, the real CONDITIONS EVALUATION REPORT, @ConditionalOnMissingBean back-off, the bean lifecycle and scopes, application.properties versus YAML, profiles, property precedence, and @SpringBootTest against a narrow slice test.

[Advanced Java] SOLID Principles in Java: Five Rules and When to Break Them

The five SOLID principles in Java on OpenJDK 21, each with a before and after that compiles and runs: a class split by its reasons to change, a growing switch replaced by an interface, a subclass that breaks its caller with no warning, an UnsupportedOperationException the compiler could have prevented, a class that cannot run without a file, and where each principle stops paying for itself.

[Advanced Java] Unit Testing in Java with JUnit 5

Unit testing in Java with JUnit 5.11.3 on OpenJDK 21: the Platform, Jupiter and Vintage split, the lifecycle callbacks, one new test instance per method, real assertion failure messages, assertThrows and assertAll, DisplayName, Nested, Disabled and Tag, parameterized tests with every argument source, assumptions versus assertions, and the habits that make a test worthless.