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.
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
What you’ll learn · Before you start
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
On this page
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.
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.
- 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.
- 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.
- 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.
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();
}
} 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:
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()andeq(…)match arguments by a rule instead of byequals. ArgumentCaptorrecords the argument of a call so that the test can check it afterwards.MockitoExtensioncreates@Mockfields 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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?
-
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?
-
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?
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
Clockand give testsClock.fixed(…), so “today” is the same everywhere. - Mockito’s
mock,when(…).thenReturn(…)andverify(…)stub and check calls; attach Mockito as a Java agent on JDK 21 and later.
References
- Mocks Aren't Stubs: the five kinds of test double (Martin Fowler)
- Mockito 5.24.0 Javadoc: Mockito, including section 0.3 on the Java agent (Mockito project)
- Mockito JUnit Jupiter 5.24.0 Javadoc: MockitoExtension (Mockito project)
- Mockito framework site (Mockito project)
- Clock (Java SE 25) (Oracle)
- JUnit User Guide 6.1.3: test interfaces and default methods (contract tests) (The JUnit Team)
- JEP 451: Prepare to Disallow the Dynamic Loading of Agents (OpenJDK)
- The java command (Java SE 25): class data sharing and -javaagent (Oracle)
- Maven Surefire Plugin: surefire:test, argLine (The Apache Software Foundation)
- Apache Maven Dependency Plugin: dependency:properties (The Apache Software Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress