Lesson 22 of 71
Mockito
A unit test checks one class, but that class usually calls others: a repository, a payment gateway, a clock. A test double is an object that stands in for such a collaborator during a test. This lesson covers the kinds of test doubles and Mockito, the library most Java code bases use to create them. Every example is a set of JUnit tests run by the launcher helper from JUnit 5, Example 1, repeated at the end of each program with one change: a failure line shows the exception class instead of the message, because Mockito messages start with a blank line and span many lines.
| Double | What it does |
|---|---|
| Stub | Returns canned answers; the test checks the result of the class under test |
| Mock | Records calls; the test verifies which calls happened |
| Spy | Wraps a real object, records calls, and can override some methods |
| Fake | A working, simplified implementation (for example an in-memory repository) |
The examples use Mockito 5.20. Mockito may print a warning about self-attaching its Java agent to standard error; it does not affect the results.
Example 1: a hand-written stub and fake
Test doubles do not need a library. The class under test depends on an interface, so a test can pass any implementation. Here a stub returns a fixed price and a fake keeps orders in a HashMap.
import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
interface PriceSource { long priceCents(String symbol); }
interface OrderRepository {
void save(String id, long totalCents);
Optional<Long> find(String id);
}
class OrderService {
private final PriceSource prices;
private final OrderRepository repo;
OrderService(PriceSource prices, OrderRepository repo) { this.prices = prices; this.repo = repo; }
long place(String id, String symbol, int qty) {
long total = prices.priceCents(symbol) * qty;
repo.save(id, total);
return total;
}
}
class InMemoryOrderRepository implements OrderRepository { // fake
final Map<String, Long> rows = new HashMap<>();
public void save(String id, long totalCents) { rows.put(id, totalCents); }
public Optional<Long> find(String id) { return Optional.ofNullable(rows.get(id)); }
}
class OrderServiceTest {
PriceSource stub = symbol -> 15_000; // stub
InMemoryOrderRepository repo = new InMemoryOrderRepository();
OrderService service = new OrderService(stub, repo);
@Test void totalIsPriceTimesQuantity() {
assertEquals(30_000, service.place("o1", "AAPL", 2));
}
@Test void orderIsStored() {
service.place("o1", "AAPL", 3);
assertEquals(Optional.of(45_000L), repo.find("o1"));
}
}
public class HandWrittenDoubles {
public static void main(String[] args) {
run(OrderServiceTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=2 succeeded=2 failed=0 aborted=0 skipped=0The second test checks state (what is in the repository afterwards). Mocks, in the next examples, check interactions (which methods were called). State checks survive refactoring better, because they do not depend on how the class reaches the result.
Example 2: mock, when/thenReturn, and thenThrow
Mockito.mock(Type.class) creates a mock: a generated subclass or instrumented class whose methods do nothing and return defaults. when(call).thenReturn(value) stubs a method for specific arguments. Chained answers apply to consecutive calls, and the last one repeats.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
interface RateService {
double rate(String from, String to);
List<String> currencies();
Optional<String> region(String currency);
String name();
}
class StubbingTest {
RateService rates = mock(RateService.class);
@Test void defaults() {
assertEquals(0.0, rates.rate("EUR", "USD"));
assertEquals(List.of(), rates.currencies());
assertEquals(Optional.empty(), rates.region("EUR"));
assertNull(rates.name());
}
@Test void stubbedForMatchingArguments() {
when(rates.rate("EUR", "USD")).thenReturn(1.08);
assertEquals(1.08, rates.rate("EUR", "USD"));
assertEquals(0.0, rates.rate("GBP", "USD")); // other arguments: default
}
@Test void consecutiveAnswers() {
when(rates.rate("EUR", "USD")).thenReturn(1.08, 1.09)
.thenThrow(new IllegalStateException("feed down"));
assertEquals(1.08, rates.rate("EUR", "USD"));
assertEquals(1.09, rates.rate("EUR", "USD"));
var e = assertThrows(IllegalStateException.class, () -> rates.rate("EUR", "USD"));
assertEquals("feed down", e.getMessage());
}
}
public class StubbingDemo {
public static void main(String[] args) {
run(StubbingTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0Unstubbed methods return "empty" values: 0 and false for primitives, null for most objects, and empty collections, Optional, and streams for those types (Mockito's RETURNS_DEFAULTS). For a method returning void, the stubbing form is doThrow(ex).when(mock).method(args), because when(...) needs a return value.
Example 3: thenAnswer
thenAnswer computes the result from the actual call. The InvocationOnMock gives access to the arguments, so one stubbing covers many inputs. A typical use is a repository save that returns its argument with a generated id.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
record Account(Long id, String owner) {}
interface AccountRepository { Account save(Account a); }
class AnswerTest {
@Test void saveAssignsIds() {
AccountRepository repo = mock(AccountRepository.class);
AtomicLong ids = new AtomicLong(100);
when(repo.save(any(Account.class))).thenAnswer(inv -> {
Account a = inv.getArgument(0);
return new Account(ids.incrementAndGet(), a.owner());
});
assertEquals(new Account(101L, "ann"), repo.save(new Account(null, "ann")));
assertEquals(new Account(102L, "bob"), repo.save(new Account(null, "bob")));
}
}
public class AnswerDemo {
public static void main(String[] args) {
run(AnswerTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=1 succeeded=1 failed=0 aborted=0 skipped=0any(Account.class) is an argument matcher, explained in Example 5. When an answer grows to more than a few lines, it is a sign that a fake (Example 8) would be clearer.
Example 4: verify with counts and order
verify(mock).method(args) fails unless that call happened exactly once. A second argument changes the count: times(n), never(), atLeast(n), atMost(n). inOrder(mocks...) checks that calls happened in a given order, and verifyNoMoreInteractions fails if any call was not verified.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
interface Ledger { void debit(String acct, long cents); void credit(String acct, long cents); }
interface Notifier { void send(String msg); }
class Transfers {
private final Ledger ledger; private final Notifier notifier;
Transfers(Ledger l, Notifier n) { ledger = l; notifier = n; }
void transfer(String from, String to, long cents) {
ledger.debit(from, cents);
ledger.credit(to, cents);
if (cents >= 1_000_000) notifier.send("large transfer " + from);
}
void batch(List<Long> amounts) { amounts.forEach(a -> transfer("A", "B", a)); }
}
class VerifyTest {
Ledger ledger = mock(Ledger.class);
Notifier notifier = mock(Notifier.class);
Transfers transfers = new Transfers(ledger, notifier);
@Test void debitBeforeCredit() {
transfers.transfer("A", "B", 500);
InOrder order = inOrder(ledger);
order.verify(ledger).debit("A", 500);
order.verify(ledger).credit("B", 500);
verify(notifier, never()).send(anyString());
}
@Test void countsInBatch() {
transfers.batch(List.of(100L, 200L, 2_000_000L));
verify(ledger, times(3)).debit(eq("A"), anyLong());
verify(notifier, times(1)).send("large transfer A");
}
@Test void failsOnPurpose() {
transfers.transfer("A", "B", 500);
verify(ledger).credit("B", 999);
}
}
public class VerifyDemo {
public static void main(String[] args) {
run(VerifyTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=2 failed=1 aborted=0 skipped=0
FAILED failsOnPurpose(): ArgumentsAreDifferentThe failure message says the arguments differed and shows both the wanted and the actual call. Verifying every call couples a test to the implementation; verify the calls that are the visible effect of the code (a payment sent, a message published), and use stubs for queries.
Example 5: argument matchers and ArgumentCaptor
Argument matchers (any(), anyString(), eq(v), argThat(predicate)) match a range of arguments in when and verify. If one argument uses a matcher, every argument must: raw values are then wrapped with eq. Mixing them throws InvalidUseOfMatchersException.
An ArgumentCaptor captures the actual argument so the test can assert on it with normal assertions. It suits objects built inside the class under test, which the test cannot construct in advance.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
record OrderEvent(String id, String side, long qty) {}
interface EventBus { void publish(String topic, OrderEvent e); }
class Exchange {
private final EventBus bus; private int seq;
Exchange(EventBus bus) { this.bus = bus; }
void buy(long qty) { bus.publish("orders", new OrderEvent("ord-" + (++seq), "BUY", qty)); }
}
class MatcherTest {
EventBus bus = mock(EventBus.class);
Exchange exchange = new Exchange(bus);
@Test void matchers() {
exchange.buy(10);
verify(bus).publish(eq("orders"), argThat(e -> e.qty() == 10 && e.side().equals("BUY")));
}
@Test void captor() {
exchange.buy(10);
exchange.buy(20);
ArgumentCaptor<OrderEvent> captor = ArgumentCaptor.forClass(OrderEvent.class);
verify(bus, times(2)).publish(eq("orders"), captor.capture());
assertEquals("ord-2", captor.getValue().id()); // last value
assertEquals(2, captor.getAllValues().size());
}
@Test void mixingIsAnError() {
exchange.buy(10);
assertThrows(InvalidUseOfMatchersException.class,
() -> verify(bus).publish("orders", any())); // raw "orders" plus a matcher
}
}
public class MatcherDemo {
public static void main(String[] args) {
run(MatcherTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0Example 6: spies
spy(object) wraps a real object. Unstubbed methods call the real code, and calls are recorded for verify. On a spy, when(spy.method()) calls the real method once while stubbing, which can throw or have side effects; doReturn(value).when(spy).method() stubs without calling it.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class SpyTest {
@Test void realBehaviourIsKept() {
List<String> list = spy(new ArrayList<String>());
list.add("a");
list.add("b");
assertEquals(2, list.size()); // real method
verify(list).add("a");
}
@Test void doReturnAvoidsRealCall() {
List<String> list = spy(new ArrayList<String>());
doReturn("stubbed").when(list).get(0); // empty list, real get(0) would throw
assertEquals("stubbed", list.get(0));
}
@Test void whenCallsRealMethod() {
List<String> list = spy(new ArrayList<String>());
assertThrows(IndexOutOfBoundsException.class,
() -> when(list.get(0)).thenReturn("never"));
}
}
public class SpyDemo {
public static void main(String[] args) {
run(SpyTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0A spy is useful for legacy classes that cannot be split, or to check one call on a real object. A test that needs many spied methods usually indicates a class with too many responsibilities.
Example 7: @Mock, @InjectMocks, and strict stubs
@ExtendWith(MockitoExtension.class) registers Mockito's JUnit extension (the extension model from the previous lesson). It creates a mock for every @Mock field before each test and builds the @InjectMocks field by passing the mocks to its largest constructor (falling back to setter and field injection).
The extension uses strict stubs by default. A stubbing that the test never uses fails the test with UnnecessaryStubbingException, and a call to a stubbed method with different arguments throws PotentialStubbingProblem. Both catch tests that no longer test what they claim to.
MockitoExtension ships in the separate mockito-junit-jupiter jar.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
interface RiskLimits { long maxNotional(String trader); }
interface Audit { void reject(String trader, long notional); }
class PreTradeCheck {
private final RiskLimits limits; private final Audit audit;
PreTradeCheck(RiskLimits limits, Audit audit) { this.limits = limits; this.audit = audit; }
boolean allow(String trader, long notional) {
boolean ok = notional <= limits.maxNotional(trader);
if (!ok) audit.reject(trader, notional);
return ok;
}
}
@ExtendWith(MockitoExtension.class)
class PreTradeCheckTest {
@Mock RiskLimits limits;
@Mock Audit audit;
@InjectMocks PreTradeCheck check;
@Test void rejectsAboveLimit() {
when(limits.maxNotional("t1")).thenReturn(1_000L);
assertFalse(check.allow("t1", 5_000));
verify(audit).reject("t1", 5_000);
}
@Test void unusedStubbing() {
when(limits.maxNotional("t1")).thenReturn(1_000L);
when(limits.maxNotional("t9")).thenReturn(1L); // never used
assertTrue(check.allow("t1", 10));
}
@Test void argumentMismatch() {
when(limits.maxNotional("t1")).thenReturn(1_000L);
check.allow("t2", 10); // stubbed for t1 only
}
}
public class ExtensionMocksDemo {
public static void main(String[] args) {
run(PreTradeCheckTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=1 failed=2 aborted=0 skipped=0
FAILED argumentMismatch(): UnnecessaryStubbingException
FAILED unusedStubbing(): UnnecessaryStubbingExceptionBoth strict tests fail with UnnecessaryStubbingException. In argumentMismatch the t1 stubbing is never used. Mockito 5.20 throws PotentialStubbingProblem only when the mismatching call comes from a different source file than the stubbing (DefaultStubbingLookupListener compares the two locations), which is the usual case of a test file and a production file. In this one-file program both are in the same file, so the problem is reported after the test as an unused stubbing instead. Without strict stubs, argumentMismatch would get the default 0 and pass for the wrong reason. A stubbing that is intentionally shared and not always used can be marked lenient().when(...).
Example 8: final classes, and when a fake is better
Since Mockito 5 the default mock maker is the inline mock maker. It changes the bytecode of the mocked class with a Java agent instead of generating a subclass, so final classes, final methods, and records can be mocked. (Mockito 2 to 4 needed an opt-in for this.) That is why Mockito attaches an agent and may print the warning mentioned at the top.
The second test class shows the same scenario with a fake. A fake has real behavior, so the test reads like a use of the system and does not repeat its call sequence.
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
final class FxQuotes { // final: no subclass possible
double mid(String pair) { throw new UnsupportedOperationException("network"); }
}
interface Positions {
void add(String symbol, long qty);
long get(String symbol);
}
final class InMemoryPositions implements Positions { // fake
private final Map<String, Long> map = new HashMap<>();
public void add(String s, long q) { map.merge(s, q, Long::sum); }
public long get(String s) { return map.getOrDefault(s, 0L); }
}
class Book {
private final Positions positions;
Book(Positions p) { positions = p; }
void fill(String symbol, long qty, boolean buy) { positions.add(symbol, buy ? qty : -qty); }
}
class FinalMockTest {
@Test void mockFinalClass() {
FxQuotes quotes = mock(FxQuotes.class);
when(quotes.mid("EURUSD")).thenReturn(1.0850);
assertEquals(1.0850, quotes.mid("EURUSD"));
}
}
class BookWithMockTest {
@Test void fills() {
Positions p = mock(Positions.class);
new Book(p).fill("AAPL", 100, true);
new Book(p).fill("AAPL", 40, false);
verify(p).add("AAPL", 100);
verify(p).add("AAPL", -40); // checks calls, not the net position
}
}
class BookWithFakeTest {
@Test void fills() {
Positions p = new InMemoryPositions();
Book book = new Book(p);
book.fill("AAPL", 100, true);
book.fill("AAPL", 40, false);
assertEquals(60, p.get("AAPL")); // checks the result
}
}
public class FinalAndFakeDemo {
public static void main(String[] args) {
run(FinalMockTest.class, BookWithMockTest.class, BookWithFakeTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ f.getException().getClass().getSimpleName())
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0Criteria for choosing:
| Situation | Better choice |
|---|---|
| Collaborator with simple state (repository, cache, queue) used by many tests | Fake, written once |
| The call itself is the effect (send email, publish event, charge card) | Mock with verify |
| A query whose result steers the code | Stub (when/thenReturn) |
| Rare error paths (timeouts, exceptions) | Stub with thenThrow |
| Types the team does not own (HTTP clients, SDKs) | Wrap them in an owned interface, then fake or mock that |
Interview points
- Stub: canned answers, state check. Mock: interaction check. Spy: real object plus recording. Fake: working lightweight implementation.
- Unstubbed mock methods return 0, false, null, or empty collections and
Optional. when(x).thenReturn(a, b)returnsa, thenbfor every later call.doReturn/doThrow().when(mock)is needed forvoidmethods and for spies.- With matchers, all arguments must be matchers; use
eqfor literal values. ArgumentCaptorcaptures values for normal assertions;getValuereturns the last one.MockitoExtensionuses strict stubs: unused stubbings fail the test, and a call with mismatching arguments from another source file throwsPotentialStubbingProblem.- Mockito 5 uses the inline mock maker by default, so final classes and records can be mocked.
- Over-mocking ties tests to implementation details; a refactor that keeps behavior breaks them.
Summary
- Depending on interfaces makes any test double possible, with or without a library.
mock,when,thenReturn,thenThrow, andthenAnswerdefine stubbed behavior.verifywithtimes,never,atLeast, andinOrderchecks interactions.argThatandArgumentCaptorinspect arguments created inside the code under test.- Spies call real methods unless stubbed with
doReturn. @Mockand@InjectMockswithMockitoExtensionremove setup code.- Fakes check results and survive refactoring; mocks check calls.
