Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 14 – Testing, build tools and code quality

Test doubles, fakes and Mockito

Choose between stubs, fakes, spies and mocks in Java, test a booking service with an in-memory fake and a fixed Clock, and verify calls with Mockito.

  • Intermediate
  • 32 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 11 of 32 minutes free
Free: Five kinds of test double · A fake repository and a fixed clock · A first Mockito test. With a pass: 3 in-depth parts and 3 interview answers, about 21 minutes.

What you will learn

  • Choose between stubs, fakes, spies and mocks
  • Use Mockito to stub calls and verify interactions
  • Choose in-memory fakes and an injected Clock for stable tests

Before you start

A test double is any object that stands in for a real dependency during a test, as a stunt double stands in for an actor. Test doubles let a test run code that normally talks to a database, an SMS provider or the system clock, without any of them: the test hands the code a double, then checks the result or what the code did with the double. Test doubles come in five kinds. Fakes are working, simplified implementations, such as a repository that keeps its data in a list, and they are usually the best double for an interface your own code defines. Mockito is a widely used Java library that creates stubs, spies and mocks at run time, which suits checking calls to systems you do not control. This lesson tests a desk booking service with a fake, a fixed Clock and Mockito, on JDK 25 with JUnit 6.1.3, AssertJ 3.27.7 and Mockito 5.24.0.

Five kinds of test double

The names come from Gerard Meszaros’s book on xUnit test patterns, as Martin Fowler’s article on mocks and stubs sums them up:

Kind What it does Checks how it was called? In this lesson
Dummy Fills a parameter and is never used No Not needed here: any object passed only to fill a parameter
Stub Returns answers fixed in advance No Clock.fixed(…), or a Mockito mock with when(…).thenReturn(…)
Spy Does little and records the calls it gets Afterwards, by the test RecordingNotifier, written by hand
Mock Is told which calls to expect, and checks them Yes A Mockito mock checked with verify(…)
Fake Works, in a simpler way than production No InMemoryBookings, a repository kept in a list

The difference that matters most in practice is between checking state, what the code produced or stored, and checking behaviour, which calls it made. Fakes, stubs and spies support the first; only mocks insist on the second. Tests that check state survive refactorings that keep the result the same, while tests that check every call break whenever the code calls its dependencies differently.

A fake repository and a fixed clock

DeskBookingService books a desk for a day. It refuses a day in the past and a desk that is taken, saves the booking and sends a confirmation. It receives its three dependencies in its constructor, which is what lets a test replace each one.

DeskBookingService uses a repository, a notifier and a clock; tests replace them with a fake, a spy or mock, and a fixed clock.DeskBookingServicethe code under testIts dependenciesBookingRepositoryreal: a databasetest: InMemoryBookings,a fakeNotifierreal: SMS or e-mailtest: RecordingNotifier,a spy, or a Mockito mockClockreal: the system clocktest: Clock.fixed(…),a stubcalls

The booking service and the doubles its tests use

Text description of the diagram

At the top is DeskBookingService, the code under test. An arrow goes from it to its three dependencies, stacked below it, each of which it receives in its constructor.

  1. BookingRepository, which the service asks whether a desk is booked and tells to save a booking. In production it is backed by a database; in the tests it is InMemoryBookings, a fake.
  2. Notifier, which the service asks to send a confirmation. In production it calls an SMS or e-mail provider; in the tests it is RecordingNotifier, a hand-written spy, or a Mockito mock.
  3. Clock, which tells the service what day it is. In production it is the system clock; in the tests it is Clock.fixed, which always gives the same instant.
The service, the fake, the spy and the tests

desks/com/example/desks/DeskBookingService.java

package com.example.desks;

import java.time.Clock;
import java.time.LocalDate;

/** Books desks in a shared office: never a day in the past, never a desk that is taken. */
public final class DeskBookingService {

    private final BookingRepository bookings;
    private final Notifier notifier;
    private final Clock clock;

    public DeskBookingService(BookingRepository bookings, Notifier notifier, Clock clock) {
        this.bookings = bookings;
        this.notifier = notifier;
        this.clock = clock;
    }

    public Booking book(String member, String desk, LocalDate day) {
        LocalDate today = LocalDate.now(clock); // today where the office is, from the clock it was given
        if (day.isBefore(today)) {
            throw new IllegalArgumentException("cannot book a day in the past: " + day);
        }
        if (bookings.isBooked(desk, day)) {
            throw new IllegalStateException("desk " + desk + " is taken on " + day);
        }
        Booking booking = new Booking(member, desk, day);
        bookings.save(booking);
        notifier.send(member, "Desk " + desk + " is yours on " + day);
        return booking;
    }
}

desks/com/example/desks/InMemoryBookings.java

package com.example.desks;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

/** A fake: a real, working repository that keeps its bookings in a list instead of a database. */
public final class InMemoryBookings implements BookingRepository {

    private final List<Booking> saved = new ArrayList<>();

    @Override
    public boolean isBooked(String desk, LocalDate day) {
        return saved.stream().anyMatch(b -> b.desk().equals(desk) && b.day().equals(day));
    }

    @Override
    public void save(Booking booking) {
        saved.add(booking);
    }

    @Override
    public List<Booking> forMember(String member) {
        return saved.stream().filter(b -> b.member().equals(member)).toList();
    }

    @Override
    public List<Booking> bookingsOn(LocalDate day) {
        return saved.stream().filter(b -> b.day().equals(day)).toList();
    }
}

desks/com/example/desks/RecordingNotifier.java

package com.example.desks;

import java.util.ArrayList;
import java.util.List;

/** A spy written by hand: it does nothing but remember every message it was asked to send. */
public final class RecordingNotifier implements Notifier {

    public final List<String> sent = new ArrayList<>();

    @Override
    public void send(String member, String message) {
        sent.add(member + ": " + message);
    }
}

desks/com/example/desks/DeskBookingServiceTest.java

package com.example.desks;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class DeskBookingServiceTest {

    // 10:00 in the office's time zone: "today" is the same on every computer and in every year
    private static final Clock TEN_AM = Clock.fixed(Instant.parse("2028-03-01T04:30:00Z"), ZoneId.of("Asia/Kolkata"));
    private static final LocalDate TODAY = LocalDate.of(2028, 3, 1);

    private InMemoryBookings bookings;
    private RecordingNotifier notifier;
    private DeskBookingService service;

    @BeforeEach
    void setUp() {
        bookings = new InMemoryBookings();
        notifier = new RecordingNotifier();
        service = new DeskBookingService(bookings, notifier, TEN_AM);
    }

    @Test
    void booksAFreeDeskAndConfirmsIt() {
        service.book("asha", "D-12", TODAY);

        assertThat(bookings.forMember("asha")).containsExactly(new Booking("asha", "D-12", TODAY));
        assertThat(notifier.sent).containsExactly("asha: Desk D-12 is yours on 2028-03-01");
    }

    @Test
    void refusesADeskThatIsTaken() {
        service.book("asha", "D-12", TODAY);

        assertThatThrownBy(() -> service.book("ravi", "D-12", TODAY)).isInstanceOf(IllegalStateException.class);
        assertThat(bookings.forMember("ravi")).isEmpty();
        assertThat(notifier.sent).hasSize(1);
    }

    @Test
    void refusesYesterday() {
        assertThatThrownBy(() -> service.book("asha", "D-12", TODAY.minusDays(1)))
                .hasMessage("cannot book a day in the past: 2028-02-29");
        assertThat(notifier.sent).isEmpty();
    }
}
The fake and the spy, then a first Mockito test Python · desks_session.py
from session import launch, libraries, run

libraries("desks")
run(f"""
cd desks
ls lib
javac -d classes -cp 'lib/*' com/example/desks/*.java
# The fake repository and the hand-written spy: no mocking library involved.
{launch("--select-class com.example.desks.DeskBookingServiceTest", agent=False)}
# Mockito doubles, with Mockito attached as a Java agent when the JVM starts.
{launch("--select-class com.example.desks.ConfirmationTest")}
""")

Output

$ cd desks
$ ls lib
assertj-core-3.27.7.jar
byte-buddy-1.17.7.jar
byte-buddy-agent-1.17.7.jar
junit-platform-console-standalone-6.1.3.jar
mockito-core-5.24.0.jar
mockito-junit-jupiter-5.24.0.jar
objenesis-3.3.jar
$ javac -d classes -cp 'lib/*' com/example/desks/*.java
# The fake repository and the hand-written spy: no mocking library involved.
$ java -cp 'lib/*:classes' org.junit.platform.console.ConsoleLauncher execute --select-class com.example.desks.DeskBookingServiceTest --include-engine junit-jupiter --disable-banner
╷
└─ JUnit Jupiter ✔
   └─ DeskBookingServiceTest ✔
      ├─ refusesYesterday() ✔
      ├─ refusesADeskThatIsTaken() ✔
      └─ booksAFreeDeskAndConfirmsIt() ✔

Test run finished after … ms
[         2 containers found      ]
[         0 containers skipped    ]
[         2 containers started    ]
[         0 containers aborted    ]
[         2 containers successful ]
[         0 containers failed     ]
[         3 tests found           ]
[         0 tests skipped         ]
[         3 tests started         ]
[         0 tests aborted         ]
[         3 tests successful      ]
[         0 tests failed          ]

# Mockito doubles, with Mockito attached as a Java agent when the JVM starts.
$ java -javaagent:lib/mockito-core-5.24.0.jar -cp 'lib/*:classes' org.junit.platform.console.ConsoleLauncher execute --select-class com.example.desks.ConfirmationTest --include-engine junit-jupiter --disable-banner
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
╷
└─ JUnit Jupiter ✔
   └─ ConfirmationTest ✔
      ├─ sendsNothingWhenTheDeskIsTaken() ✔
      └─ sendsOneConfirmation() ✔

Test run finished after … ms
[         2 containers found      ]
[         0 containers skipped    ]
[         2 containers started    ]
[         0 containers aborted    ]
[         2 containers successful ]
[         0 containers failed     ]
[         2 tests found           ]
[         0 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 desks_session.py

InMemoryBookings is a fake: save a booking and it really answers that the desk is taken that day, so the second test could book a desk and then try again. RecordingNotifier is a spy: it sends nothing and remembers every message, so a test can read exactly what a member would have received. Clock.fixed(…) stubs time: the service asks LocalDate.now(clock), and the answer is the same day on every computer, in every time zone and in every year, so refusesYesterday knows which day yesterday was. All three tests used only plain Java objects, without any mocking library.

A first Mockito test

ConfirmationTest checks the same service with Mockito, which ran second in the session above:

ConfirmationTest.java Java · desks/com/example/desks/ConfirmationTest.java
package com.example.desks;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import org.junit.jupiter.api.Test;

/** The same service with Mockito doubles: a stubbed repository and a mock notifier whose calls are verified. */
class ConfirmationTest {

    private static final Clock TEN_AM = Clock.fixed(Instant.parse("2028-03-01T04:30:00Z"), ZoneId.of("Asia/Kolkata"));
    private static final LocalDate TODAY = LocalDate.of(2028, 3, 1);

    @Test
    void sendsOneConfirmation() {
        BookingRepository bookings = mock(BookingRepository.class); // every method answers false, 0, null or empty
        Notifier notifier = mock(Notifier.class);
        DeskBookingService service = new DeskBookingService(bookings, notifier, TEN_AM);

        service.book("asha", "D-12", TODAY);

        verify(notifier).send("asha", "Desk D-12 is yours on 2028-03-01");
    }

    @Test
    void sendsNothingWhenTheDeskIsTaken() {
        BookingRepository bookings = mock(BookingRepository.class);
        when(bookings.isBooked("D-12", TODAY)).thenReturn(true);
        Notifier notifier = mock(Notifier.class);
        DeskBookingService service = new DeskBookingService(bookings, notifier, TEN_AM);

        assertThrows(IllegalStateException.class, () -> service.book("ravi", "D-12", TODAY));

        verify(notifier, never()).send(anyString(), anyString());
    }
}

mock(Notifier.class) creates an object that implements the interface and records every call. An unstubbed method returns a default, false, 0, an empty collection or null, which is why the repository mock answered that the desk was free. when(…).thenReturn(…) stubs one answer for one set of arguments. verify(notifier).send(…) passes only if that call happened exactly once, and never() checks that it did not happen at all.

The session started the JVM with -javaagent:lib/mockito-core-5.24.0.jar. On Java 21 and later, Mockito’s documentation asks for this setup, because the JDK now warns when a library attaches itself as an agent while the program runs. The line about class sharing is a note from the JVM: Mockito added classes to the boot class path, so the JVM now uses its archive of prepared classes (class data sharing) for the boot loader’s classes only, which costs a little start-up time and nothing else.

The table leads to a rule of thumb: use mocks where the call is the behaviour, such as “a confirmation was sent once”, and real objects or fakes elsewhere. Mockito’s own documentation tells readers not to mock a List but to use a real one. Mock types you do not own, such as a vendor’s SDK, sparingly, and write fakes for the interfaces your code defines.

Four Mockito tools that the rest of this lesson uses, in one sentence each:

  • Argument matchers such as anyString() and eq(…) match arguments by a rule instead of by equals.
  • ArgumentCaptor records the argument of a call so that the test can check it afterwards.
  • MockitoExtension creates @Mock fields for each test and fails tests whose stubs were never used.
  • A contract test is one set of tests run against both the fake and the real implementation.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 InMemoryBookings keeps bookings in a list and really answers isBooked from what was saved. What kind of test double is it?

    Choose one answer.

    Show the answer to question 1

    Answer: A fake: a working, simpler implementation

    A fake has real behaviour, just a lighter implementation: save a booking and isBooked answers true for that desk and day. A stub would answer whatever the test fixed in advance, whatever was saved.

  2. Question 2 of 4 A Mockito mock of BookingRepository has no stubbing. What does isBooked("D-12", day) return?

    Choose one answer.

    Show the answer to question 2

    Answer: false

    An unstubbed mock method returns a default for its type: false for boolean, 0 for numbers, an empty collection for collections and null for other objects. That is why sendsOneConfirmation could book the desk without any stubbing.

  3. Question 3 of 4 Why does DeskBookingService take a Clock instead of calling LocalDate.now()?

    Choose one answer.

    Show the answer to question 3

    Answer: So that a test can fix "today" and get the same answer on every computer, in every time zone, on every day

    LocalDate.now() reads the machine's clock and time zone, so a test of "a day in the past" would depend on when and where it runs. With Clock.fixed, refusesYesterday knows exactly which day yesterday was.

  4. Question 4 of 4 Which call checks that no confirmation was sent at all?

    Choose one answer.

    Show the answer to question 4

    Answer: verify(notifier, never()).send(anyString(), anyString())

    never() is times(0), and anyString() matches any text, so the check fails if send was called with any arguments. sendsNothingWhenTheDeskIsTaken used exactly this.

Mockito in depth: the extension, captors and strict stubs Pro: opens with a Pro, Premium or Ultimate pass

Mockito’s API is small, but its failure messages and its strict mode are where the time goes in real projects. This part uses the JUnit extension on the booking service, reproduces a design flaw that only a mock exposes cleanly, and triggers the three misuse errors that teams meet most.

In this part:

  • Mocks, captors and the order of calls with MockitoExtension
  • A failing SMS provider and a booking that was saved anyway, reproduced
  • Matchers and plain values cannot mix, reproduced
  • Strict stubs: an unused stub and a stub for the wrong day, reproduced
  • Stubbing styles: a comparison table

6 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

When mocks hurt: a refactoring, a lying fake and contract tests Pro: opens with a Pro, Premium or Ultimate pass

Mocks make it easy to write tests that pin down how code works instead of what it does. This part reproduces a refactoring that breaks such tests while every fake-based test stays green, then shows how a fake can lie and how a contract test catches it.

In this part:

  • A refactoring that breaks the mock tests and not the fakes, reproduced
  • One contract for the fake and the real repository
  • A fake that lies, and the contract test that catches it
  • Which double for which dependency: a trade-off table

5 runnable examples (BookingRepositoryContract.java, FileBookings.java, FileBookingsContractTest.java, InMemoryBookingsContractTest.java, OverSpecifiedTest.java, Python, refactored/DeskBookingService.java) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Mockito on JDK 25, its Maven setup and a slow-service exercise Pro: opens with a Pro, Premium or Ultimate pass

Mockito 5 instruments classes through a Java agent, and recent JDKs are closing the door to agents that attach themselves at run time. This part shows the warning, the Maven configuration that removes it and a copy-paste mistake that crashes the test JVM, then sets an exercise on a converter that calls a slow service.

In this part:

  • Mockito attaching itself at run time, reproduced
  • The Maven setup, and the argLine copy that crashes the test JVM
  • Exercise: fast tests for a converter that calls a slow service

5 runnable examples (CheckRates.java, Python, RateConverter.java, pom-late-argline.xml, pom.xml, solution/MutableClock.java, solution/RateConverterTest.java, starter/RateConverterTest.java) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    What is the difference between a stub and a mock?

    Both stand in for a real dependency, but they check different things. A stub only answers: it returns values fixed in advance, such as a clock that always says the same time, and the test then checks the result the code produced, its state. A mock is told which calls to expect and fails the test when they do not happen, so the test checks the code's behaviour, the calls it made. In Mockito one object can be both: when(...).thenReturn(...) stubs it, verify(...) uses it as a mock. Prefer stubs and fakes with state checks, which survive refactorings, and keep mocks for calls that are the behaviour itself, such as one confirmation sent or one payment requested.

  2. Mid to senior

    After a refactoring that changes no behaviour, 40 tests fail with "Wanted but not invoked" and a few others now pass for the wrong reason. What does that say about the tests, and how do you fix them without losing what they protect?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to staff

    Your service calls a payment provider through the provider's Java SDK. A colleague's 30 tests mock the SDK's classes call by call. What problems do you expect, and what would you do instead?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    A booking service's tests pass on developers' laptops in India but fail on the build server, which runs in UTC, every evening from 18:30 to midnight UTC. What is the likely cause, and how do you fix the code and the tests?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A test double replaces a real dependency in a test: a dummy, a stub, a spy, a mock or a fake.
  • Prefer checking state with fakes and stubs; use mocks where the call itself is the behaviour.
  • Inject a Clock and give tests Clock.fixed(…), so “today” is the same everywhere.
  • Mockito’s mock, when(…).thenReturn(…) and verify(…) stub and check calls; attach Mockito as a Java agent on JDK 21 and later.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.