Appearance
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 type | Default value |
|---|---|
| Object reference | null |
int, long, double, float, byte, short | 0 |
boolean | false |
char | '\0' |
List, Set, Map, Collection | empty 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 6Stubbing 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 — defaultArgument 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 repeatsThis 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 rundoNothing(...).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 avoidedFor 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 nothingdoThrow(...).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
| Matcher | Description |
|---|---|
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 stubDynamic 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
thenAnswer | doAnswer | |
|---|---|---|
| Syntax | when(mock.method()).thenAnswer(inv -> ...) | doAnswer(inv -> ...).when(mock).method() |
| Use with void methods | No | Yes |
| Use with spy | Risky (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.