Mocking with Mockito in Java
Car makers test seat belts with crash-test dummies, not real drivers. The dummy sits exactly where a person would, and afterwards the engineers check what happened to it: did the belt hold, did the head hit the wheel? Testing code has the same problem. A FeeService that reads students from a database and sends emails can't be unit tested with a real database and a real mail server: the tests would be slow, need setup, and send real emails to real parents. Mockito gives you dummies for those parts. Each dummy answers exactly as you tell it to and records every call made to it, so afterwards you can check what happened. In this lesson, you will create mocks, decide what they return, verify the calls your code made, capture arguments, and read Mockito's failure messages. Writing tests is covered in "Unit Testing with JUnit in Java".
What Is Mocking?
Mocking is a testing technique in which the real collaborators of a class (the objects it calls, such as a repository or an email sender) are replaced with mock objects that are used to return prepared answers and record how they were called. Mockito is the most widely used mocking library for Java; it creates mocks of interfaces and classes at runtime.
| Test double | What it does |
|---|---|
| Dummy | Passed in only to fill a parameter; never used |
| Stub | Returns prepared answers |
| Mock | Returns prepared answers and records calls, so the test can verify them |
| Spy | Wraps a real object; real methods run unless you override some |
| Fake | A working but simple implementation, such as an in-memory repository |
A Mockito mock can act as a dummy, a stub or a mock, depending on how you use it.
The Class Under Test
FeeService looks up a student, calculates the fee, and sends a notice if the payment is more than 30 days late. It depends on two interfaces:
public interface StudentRepository {
Optional<Student> findByName(String name);
}public interface Notifier {
void send(String to, String message);
}public class FeeService {
private static final Logger log = LoggerFactory.getLogger(FeeService.class);
private final StudentRepository repo;
private final Notifier notifier;
private final FeeCalculator calc;
public FeeService(StudentRepository repo, Notifier notifier,
FeeCalculator calc) {
this.repo = repo;
this.notifier = notifier;
this.calc = calc;
}
public int bill(String name) {
Student s = repo.findByName(name).orElseThrow(
() -> new IllegalArgumentException("No student: " + name));
int total = calc.totalFee(s.baseFee(), s.lateDays());
if (s.lateDays() > 30) {
notifier.send(name, "Fee overdue: " + total);
}
log.info("Billed {} {}", name, total);
return total;
}
}The dependencies arrive through the constructor. That is what makes the class testable: a test can pass in mocks instead of the real database and email classes. Student is the record Student(String name, int baseFee, int lateDays), and FeeCalculator is the class tested in the previous lesson.
Setting Up Mockito
Add mockito-junit-jupiter, which brings mockito-core with it:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.24.0</version>
<scope>test</scope>
</dependency>On recent JDKs, the first run prints this:
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
WARNING: A Java agent has been loaded dynamically (~/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.7/byte-buddy-agent-1.17.7.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future releaseMockito changes classes while the program runs, and to do that it loads a Java agent. Since Java 21, the JDK warns when an agent is loaded in the middle of a run, and a future release will block it. The fix is to load Mockito as an agent from the start, with this Maven configuration:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<argLine>-javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
</configuration>
</plugin>Explanation: the dependency plugin's properties goal creates a property for every dependency's JAR path, so ${org.mockito:mockito-core:jar} becomes the path to Mockito's JAR. Surefire then starts the test JVM with -javaagent: pointing at it. -Xshare:off turns off class data sharing for the tests, which removes the harmless "Sharing is only supported..." line. After this change, all the warnings are gone.
Writing Tests with Mocks
@ExtendWith(MockitoExtension.class)
class FeeServiceTest {
@Mock StudentRepository repo;
@Mock Notifier notifier;
@Captor ArgumentCaptor<String> message;
FeeService service;
@BeforeEach
void setUp() {
service = new FeeService(repo, notifier, new FeeCalculator(500));
}
@Test
void billsOnTimeStudentWithoutNotice() {
when(repo.findByName("Navika"))
.thenReturn(Optional.of(new Student("Navika", 15000, 0)));
assertEquals(15000, service.bill("Navika"));
verify(notifier, never()).send(anyString(), anyString());
}
@Test
void sendsNoticeWhenVeryLate() {
when(repo.findByName("Advitha"))
.thenReturn(Optional.of(new Student("Advitha", 14000, 45)));
assertEquals(36500, service.bill("Advitha"));
verify(notifier).send(eq("Advitha"), message.capture());
assertEquals("Fee overdue: 36500", message.getValue());
}
@Test
void unknownStudentIsAnError() {
when(repo.findByName("Srihitha")).thenReturn(Optional.empty());
IllegalArgumentException e = assertThrows(
IllegalArgumentException.class,
() -> service.bill("Srihitha"));
assertEquals("No student: Srihitha", e.getMessage());
}
@Test
void databaseErrorIsPassedOn() {
when(repo.findByName(anyString()))
.thenThrow(new IllegalStateException("database down"));
assertThrows(IllegalStateException.class,
() -> service.bill("Navika"));
verify(notifier, never()).send(anyString(), anyString());
}
}[INFO] Running school.fees.FeeServiceTest
09:41:33.761 [main] INFO school.fees.FeeService -- Billed Navika 15000
09:41:33.774 [main] INFO school.fees.FeeService -- Billed Advitha 36500
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.220 s -- in school.fees.FeeServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0Explanation:
@ExtendWith(MockitoExtension.class)connects Mockito to JUnit. It creates a fresh mock for every@Mockfield before each test, and checks for unused stubs afterwards.when(repo.findByName("Navika")).thenReturn(...)is stubbing: when the service asks the mock for Navika, it gets this student. A call that wasn't stubbed returns an "empty" value:null,0,false, an empty collection, orOptional.empty().- The real
FeeCalculatoris used. Only the parts that reach outside the program are mocked; a simple class with no outside effects needs no mock. verify(notifier).send(...)checks that the method was called exactly once with those arguments.verify(notifier, never())checks that it wasn't called at all, so on-time students get no email.anyString()andeq("Advitha")are argument matchers: they match any value, or one exact value.- The
@Captorfield records the argument actually passed, so the test can check the full message text. thenThrowmakes the mock fail, to test how the service behaves when the database is down.- The log lines come from the real SLF4J logger inside
FeeService(see "Logging in Java"). The test with the missing student and the one with the database error stop before the log line.
Reading Mockito Failures
Suppose a change makes the service send notices only after 60 days instead of 30. The first three tests still pass, but sendsNoticeWhenVeryLate fails:
[ERROR] Failures:
[ERROR] FeeServiceTest.sendsNoticeWhenVeryLate:48
Wanted but not invoked:
notifier.send(
"Advitha",
<Capturing argument: String>
);
-> at school.fees.FeeServiceTest.sendsNoticeWhenVeryLate(FeeServiceTest.java:48)
Actually, there were zero interactions with this mock.
[INFO]
[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0Explanation: Mockito shows the call it wanted, the line of the verify, and what actually happened: nothing was sent. The fee was still right, so without the mock nothing would have caught the missing notice.
Mockito is also strict by default. A stub that the test never uses is reported as an error, because it usually means the test isn't testing what you think:
@Test
void stubsTooMuch() {
when(repo.findByName("Navika"))
.thenReturn(Optional.of(new Student("Navika", 15000, 0)));
when(repo.findByName("Srihitha")) // never used
.thenReturn(Optional.of(new Student("Srihitha", 15000, 2)));
FeeService service =
new FeeService(repo, notifier, new FeeCalculator(500));
assertEquals(15000, service.bill("Navika"));
}[ERROR] school.fees.ExtraStubTest.stubsTooMuch -- Time elapsed: 0.166 s <<< ERROR!
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at school.fees.ExtraStubTest.stubsTooMuch(ExtraStubTest.java:21)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.Remove the unused stub. If a shared setup method really must stub something only some tests use, mark that stub with lenient().when(...).
Mockito Methods
| Method | Description |
|---|---|
mock(Type.class) / @Mock | It is used to create a mock of an interface or class. |
when(call).thenReturn(value) | It makes the call return a value; several values are returned one after another. |
when(call).thenThrow(exception) | It makes the call throw. |
when(call).thenAnswer(inv -> ...) | It computes the answer from the arguments. |
doThrow(e).when(mock).voidMethod() | It stubs a void method, which can't go inside when(...). |
verify(mock).method(args) | It checks the method was called exactly once with these arguments. |
verify(mock, times(2)), never(), atLeastOnce() | They check how many times it was called. |
verifyNoMoreInteractions(mock) | It checks there were no calls other than those verified. |
any(), anyString(), anyInt(), eq(x) | They are argument matchers. |
ArgumentCaptor / @Captor | It captures arguments so you can assert on them. |
spy(object) / @Spy | It wraps a real object; unstubbed methods run for real. |
@InjectMocks | It creates the class under test and passes the @Mock fields into its constructor. |
What to Mock and What Not To
| Mock | Don't mock |
|---|---|
| Databases and repositories | Value objects and records such as Student |
| Email, SMS and other outside services | Simple calculation classes such as FeeCalculator |
| Web clients calling other systems | Java library types such as List or String |
The clock, when time matters (or pass a Clock) | The class under test itself |
Too many mocks make tests that only check "the code calls what the code calls", and they break on every refactoring. Mock at the edges of the program.
Common Mistakes
Verifying every call. Verify only the effects that matter to the behavior being tested; otherwise any harmless change breaks the test.
Mocking the class under test. Then you are testing Mockito, not your code.
Creating dependencies inside the class. new SmtpNotifier() inside FeeService can't be replaced by a mock. Pass dependencies in through the constructor.
Mocking static or final code without thinking. Mockito can (mockStatic), but needing it usually means the design should take a dependency instead.
Forgetting @ExtendWith(MockitoExtension.class). The @Mock fields stay null, and the test throws NullPointerException.
Practice
- Easy: Add a test that a student who is exactly 30 days late gets no notice, and one 31 days late does.
- Harder: Add a method
billAll(List<String> names)toFeeServicethat bills every student, skips unknown names, and returns the total. Test it with a mock repository that knows two of three names, and verify that the notifier was called exactly once withtimes(1).
Key Takeaways
Was this tutorial helpful?