Guide contents

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.

Java
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);
    }
}
Output
tests: found=2 succeeded=2 failed=0 aborted=0 skipped=0

The 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.

Java
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);
    }
}
Output
tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0

Unstubbed 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.

Java
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);
    }
}
Output
tests: found=1 succeeded=1 failed=0 aborted=0 skipped=0

any(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.

Java
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);
    }
}
Output
tests: found=3 succeeded=2 failed=1 aborted=0 skipped=0
FAILED failsOnPurpose(): ArgumentsAreDifferent

The 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.

Java
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);
    }
}
Output
tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0

Example 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.

Java
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);
    }
}
Output
tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0

A 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.

Java
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);
    }
}
Output
tests: found=3 succeeded=1 failed=2 aborted=0 skipped=0
FAILED argumentMismatch(): UnnecessaryStubbingException
FAILED unusedStubbing(): UnnecessaryStubbingException

Both 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.

Java
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);
    }
}
Output
tests: found=3 succeeded=3 failed=0 aborted=0 skipped=0

Criteria 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) returns a, then b for every later call.
  • doReturn/doThrow().when(mock) is needed for void methods and for spies.
  • With matchers, all arguments must be matchers; use eq for literal values.
  • ArgumentCaptor captures values for normal assertions; getValue returns the last one.
  • MockitoExtension uses strict stubs: unused stubbings fail the test, and a call with mismatching arguments from another source file throws PotentialStubbingProblem.
  • 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, and thenAnswer define stubbed behavior.
  • verify with times, never, atLeast, and inOrder checks interactions.
  • argThat and ArgumentCaptor inspect arguments created inside the code under test.
  • Spies call real methods unless stubbed with doReturn.
  • @Mock and @InjectMocks with MockitoExtension remove setup code.
  • Fakes check results and survive refactoring; mocks check calls.