Skip to content

Mockito: Different Ways of Stubbing (Matchers, Static and Dynamic Stubbing)

The Call Center Script Analogy

Imagine a call center where operators follow a rulebook. Each entry in the rulebook says: "If the caller asks question X, say answer Y." If no entry matches the caller's question, the operator gives a generic default response. Mockito stubs are exactly this rulebook. You write rules like "when calculateTax(100.0) is called, return 15.0." If a method is called with arguments that match no rule, Mockito gives the default response (null, 0, false, or an empty collection).

The richness of Mockito's stubbing API lets you write rules that match exactly, match flexibly (matchers), return sequences of values, throw exceptions, or execute custom logic. Mastering all of these is essential for writing expressive and correct tests.


Default Behavior Without Stubbing

When a method on a mock is invoked and no stub has been configured:

Return typeDefault value
Object referencenull
int, long, double, float, byte, short0
booleanfalse
char'\0'
List, Set, Map, Collectionempty collection
java
Calculator calc = Mockito.mock(Calculator.class);

// No stub — returns default for int
int result = calc.add(4, 2);
assertEquals(0, result); // default is 0, not 6

Stubbing overrides these defaults. Every stubbing style below is a way of replacing the default.


when(...).thenReturn(...)

The most common stubbing style. It says: when this method is called with these exact arguments, return this value.

java
Calculator calc = mock(Calculator.class);

// Stub exact argument match
when(calc.add(4, 2)).thenReturn(10);

assertEquals(10, calc.add(4, 2)); // stub applied
assertEquals(0,  calc.add(5, 3)); // no stub for these args — default

Argument matching is strict: the stub for add(4, 2) does not apply when add(5, 3) is called.


when(...).thenThrow(...)

Configure a method to throw an exception instead of returning a value.

Unchecked exceptions

java
when(calc.add(4, 2)).thenThrow(new ArithmeticException("overflow"));

assertThrows(ArithmeticException.class, () -> calc.add(4, 2));

Checked exceptions

For checked exceptions, the test method signature must declare the exception (or the stub must match the method's throws clause):

java
// Interface method: int readValue(String key) throws IOException;
DataStore mockStore = mock(DataStore.class);
when(mockStore.readValue("missing")).thenThrow(new IOException("not found"));

// Test method must declare or catch IOException
assertThrows(IOException.class, () -> mockStore.readValue("missing"));

when(...).thenReturn(...).thenReturn(...).thenThrow(...) — Chaining

You can chain multiple return values and exceptions. The behaviors apply in order. The last configured behavior repeats for all subsequent calls.

java
Calculator calc = mock(Calculator.class);

when(calc.add(4, 2))
    .thenReturn(5)
    .thenReturn(6)
    .thenThrow(new RuntimeException("limit exceeded"))
    .thenReturn(7);

assertEquals(5, calc.add(4, 2));                                   // 1st call
assertEquals(6, calc.add(4, 2));                                   // 2nd call
assertThrows(RuntimeException.class, () -> calc.add(4, 2));        // 3rd call
assertEquals(7, calc.add(4, 2));                                   // 4th call
assertEquals(7, calc.add(4, 2));                                   // 5th call — last repeats

This is extremely useful for testing retry logic, cache population, or any multi call sequence.


doReturn(...).when(...).method(...) — Safe for Spies

As detailed in the architecture chapter, using when(spy.method()) actually invokes the real method during stub setup. doReturn avoids this:

java
Calculator real = new Calculator();
Calculator spy = spy(real);

// SAFE: real multiply is NOT invoked during stub setup
doReturn(100).when(spy).multiply(4, 2);

assertEquals(6,   spy.add(4, 2));       // no stub — real add runs
assertEquals(100, spy.multiply(4, 2));  // stub applies — real multiply does NOT run

doNothing(...).when(...) — Stubbing Void Methods

Mock objects automatically do nothing for void methods — no stub is needed. But for spy objects, a void method would invoke the real code if not stubbed:

java
public class NotificationService {
    public void sendMail(String userId) {
        // real code: connects to SMTP server
        smtpClient.send(userId);
    }
}

NotificationService spy = spy(new NotificationService());

// Without this, spy.sendMail would actually call the SMTP server
doNothing().when(spy).sendMail(anyString());

spy.sendMail("user@example.com"); // real SMTP call avoided

For mock objects (not spies), doNothing is redundant but harmless:

java
NotificationService mock = mock(NotificationService.class);
// No stub needed — void methods on mocks already do nothing
mock.sendMail("user@example.com"); // silently does nothing

doThrow(...).when(...) — Throwing from Void Methods

thenThrow cannot be chained after a when() for void methods (there is no return type to chain on). Use doThrow instead:

java
NotificationService mock = mock(NotificationService.class);

doThrow(new RuntimeException("SMTP down")).when(mock).sendMail(anyString());

assertThrows(RuntimeException.class, () -> mock.sendMail("user@example.com"));

Argument Matchers

By default, stubs match only on exact argument values. Matchers provide flexible argument matching.

Key rule: if you use a matcher for any argument, all arguments in that method call must also be matchers. Mixing exact values with matchers causes a compile time or runtime error.

java
// WRONG: mixing exact value 2 with matcher anyInt()
when(calc.add(anyInt(), 2)).thenReturn(10); // error!

// CORRECT: use equals() for the exact value
when(calc.add(anyInt(), eq(2))).thenReturn(10);

Complete Matcher Reference

MatcherDescription
any()Any object, including null
any(Class<T>)Any object of a specific type
anyInt()Any int
anyLong()Any long
anyDouble()Any double
anyString()Any String
anyList()Any List including null
anyMap()Any Map including null
eq(value)Exact value — allows mixing with other matchers
isNull()Only null
isNotNull()Anything except null
greaterThan(n)Greater than a value
lessThan(n)Less than a value
contains(s)String containing substring
startsWith(s)String starting with prefix
endsWith(s)String ending with suffix
matches(regex)String matching a regex
java
Calculator calc = mock(Calculator.class);

// Any two ints — matches add(1,2), add(99,100), etc.
when(calc.add(anyInt(), anyInt())).thenReturn(10);

// First arg must be exactly 5, second can be anything
when(calc.add(eq(5), anyInt())).thenReturn(50);

assertEquals(50, calc.add(5, 99)); // eq(5) matches, anyInt() matches
assertEquals(10, calc.add(3, 99)); // falls through to anyInt/anyInt stub

Dynamic Stubbing: thenAnswer and doAnswer

When to use dynamic stubbing

Static stubs (thenReturn) work well when a test is stateless — the output depends only on the input to the stubbed method. But some tests are stateful — the output of a later call depends on what happened in an earlier call.

Classic example: a UserService that saves users and retrieves them. The getUser result should depend on what was saved via register:

java
public class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public void register(User user) {
        repository.save(user);
    }

    public User getUser(long id) {
        return repository.findById(id);
    }
}

Without dynamic stubbing — the weak test

java
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);

User user1 = new User(1L, "Alice");
User user2 = new User(2L, "Bob");

// Static stubs — not realistic behavior
doNothing().when(mockRepo).save(user1);
doNothing().when(mockRepo).save(user2);
when(mockRepo.findById(1L)).thenReturn(user1);
when(mockRepo.findById(2L)).thenReturn(user2);

service.register(user1);
service.register(user2);

assertEquals(user1, service.getUser(1L));
assertEquals(user2, service.getUser(2L));

This test "passes" but is meaningless: it does not verify that register actually makes users available to getUser. The stubs for findById are hardcoded regardless of whether save was ever called.

With dynamic stubbing — the proper test

java
@Test
void register_thenGetUser_shouldReturnCorrectUser() {
    UserRepository mockRepo = mock(UserRepository.class);
    UserService service = new UserService(mockRepo);

    // In-memory store to simulate realistic repository behavior
    Map<Long, User> store = new HashMap<>();

    // doAnswer: execute custom code when save is called
    doAnswer(invocation -> {
        User saved = invocation.getArgument(0); // get first argument
        store.put(saved.getId(), saved);
        return null; // save returns void
    }).when(mockRepo).save(any(User.class));

    // thenAnswer: execute custom code when findById is called
    when(mockRepo.findById(anyLong())).thenAnswer(invocation -> {
        long id = invocation.getArgument(0);
        return store.get(id);
    });

    User alice = new User(1L, "Alice");
    User bob   = new User(2L, "Bob");

    service.register(alice);
    service.register(bob);

    assertEquals(alice, service.getUser(1L)); // depends on register having been called
    assertEquals(bob,   service.getUser(2L));
    assertNull(service.getUser(99L));          // never registered
}

Now the test validates real behavior: getUser only works if register stored the user first.

thenAnswer vs doAnswer

thenAnswerdoAnswer
Syntaxwhen(mock.method()).thenAnswer(inv -> ...)doAnswer(inv -> ...).when(mock).method()
Use with void methodsNoYes
Use with spyRisky (invokes real method during setup)Safe

Complete Example Bringing It All Together

java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.IOException;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

@ExtendWith(MockitoExtension.class)
class StubbingExamplesTest {

    @Mock Calculator calculator;
    @Mock DataStore dataStore;
    @Mock NotificationService notifier;

    @Test
    void whenThenReturn_exactArgs() {
        when(calculator.add(3, 4)).thenReturn(7);
        assertEquals(7,  calculator.add(3, 4));
        assertEquals(0,  calculator.add(1, 1)); // no stub for 1,1 — default
    }

    @Test
    void thenThrow_uncheckedException() {
        when(calculator.add(anyInt(), anyInt()))
            .thenThrow(new ArithmeticException("bad"));
        assertThrows(ArithmeticException.class, () -> calculator.add(1, 2));
    }

    @Test
    void thenThrow_checkedException() throws IOException {
        when(dataStore.readValue("missing")).thenThrow(new IOException("not found"));
        assertThrows(IOException.class, () -> dataStore.readValue("missing"));
    }

    @Test
    void chaining_multipleReturns() {
        when(calculator.add(1, 1))
            .thenReturn(10)
            .thenReturn(20)
            .thenThrow(new RuntimeException("enough"));

        assertEquals(10, calculator.add(1, 1));
        assertEquals(20, calculator.add(1, 1));
        assertThrows(RuntimeException.class, () -> calculator.add(1, 1));
    }

    @Test
    void doNothing_forVoidSpy() {
        NotificationService spy = spy(new NotificationService());
        doNothing().when(spy).sendMail(anyString());
        spy.sendMail("user@test.com"); // real SMTP not called
        verify(spy).sendMail("user@test.com");
    }

    @Test
    void matchers_mixedWithEq() {
        when(calculator.add(eq(5), anyInt())).thenReturn(99);
        assertEquals(99, calculator.add(5, 1000));
        assertEquals(0,  calculator.add(4, 1000)); // eq(5) not satisfied
    }

    @Test
    void dynamicStubbing_statefulUserService() {
        UserRepository mockRepo = mock(UserRepository.class);
        UserService service = new UserService(mockRepo);

        Map<Long, User> store = new HashMap<>();

        doAnswer(inv -> {
            User u = inv.getArgument(0);
            store.put(u.getId(), u);
            return null;
        }).when(mockRepo).save(any(User.class));

        when(mockRepo.findById(anyLong()))
            .thenAnswer(inv -> store.get((Long) inv.getArgument(0)));

        User alice = new User(1L, "Alice");
        service.register(alice);

        assertEquals(alice, service.getUser(1L));
        assertNull(service.getUser(2L));
    }
}

Interview Questions & Pitfalls

Q1: What is the difference between thenReturn and doReturn?

Both configure a return value. thenReturn is used in the when(mock.method()).thenReturn(value) form. For mock objects both are equivalent. For spy objects, when(spy.method()) evaluates spy.method() first, which invokes the real implementation before the stub is registered — a dangerous side effect. doReturn(value).when(spy).method() registers the stub without invoking the real code, making it the safe choice for spies.

Q2: What is the mixing rule for argument matchers, and how do you specify an exact value alongside matchers?

If any argument in a method call uses a matcher, all arguments must use matchers. You cannot mix anyInt() with a plain literal 2. To specify an exact value alongside a matcher, use eq(2)eq() is itself a matcher that checks for equality. Example: when(calc.add(anyInt(), eq(2))).thenReturn(10).

Q3: How do you stub a void method to throw an exception?

Use doThrow: doThrow(new RuntimeException()).when(mock).voidMethod(). The thenThrow approach in when() chains does not apply to void methods because there is no return value to chain from.

Q4: When should you use dynamic stubbing (thenAnswer / doAnswer) instead of static stubbing?

Use dynamic stubbing when the output of the stubbed method must depend on what happened in a previous call — that is, when the test is stateful. Classic cases are repository patterns (save then retrieve), retry logic (first two calls fail, third succeeds), and cache behavior. For stateless scenarios where output depends only on current input, static thenReturn is simpler and sufficient.

Q5: What does invocation.getArgument(0) return inside a thenAnswer lambda?

It returns the actual argument that was passed to the stubbed method at call time, cast to the inferred type. Index 0 is the first argument, 1 the second, and so on. This gives the dynamic stub access to what the caller passed, allowing the stub to compute a response based on real inputs.

Q6: What happens when a chained stub like thenReturn(a).thenReturn(b).thenThrow(ex) runs out of configured behaviors?

The last configured behavior repeats indefinitely. If the method is called a fourth time after thenThrow(ex), the exception is thrown again. If the last item is thenReturn(b), b is returned for all subsequent calls. This is intentional: tests can predict behavior for long sequences without enumerating every single call.