Mocking with Mockito in Java

Naveen AnamaneniUpdated · 10 min read

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 doubleWhat it does
DummyPassed in only to fill a parameter; never used
StubReturns prepared answers
MockReturns prepared answers and records calls, so the test can verify them
SpyWraps a real object; real methods run unless you override some
FakeA 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:

StudentRepository.java
public interface StudentRepository {
    Optional<Student> findByName(String name);
}
Notifier.java
public interface Notifier {
    void send(String to, String message);
}
FeeService.java
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:

pom.xml
<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:

Terminal output
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 release

Mockito 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:

pom.xml
<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

FeeServiceTest.java
@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());
    }
}
Terminal output
[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: 0

Explanation:

  1. @ExtendWith(MockitoExtension.class) connects Mockito to JUnit. It creates a fresh mock for every @Mock field before each test, and checks for unused stubs afterwards.
  2. 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, or Optional.empty().
  3. The real FeeCalculator is used. Only the parts that reach outside the program are mocked; a simple class with no outside effects needs no mock.
  4. 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.
  5. anyString() and eq("Advitha") are argument matchers: they match any value, or one exact value.
  6. The @Captor field records the argument actually passed, so the test can check the full message text.
  7. thenThrow makes the mock fail, to test how the service behaves when the database is down.
  8. 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:

Terminal output
[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: 0

Explanation: 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:

ExtraStubTest.java
@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"));
}
Terminal output
[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

MethodDescription
mock(Type.class) / @MockIt 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 / @CaptorIt captures arguments so you can assert on them.
spy(object) / @SpyIt wraps a real object; unstubbed methods run for real.
@InjectMocksIt creates the class under test and passes the @Mock fields into its constructor.

What to Mock and What Not To

MockDon't mock
Databases and repositoriesValue objects and records such as Student
Email, SMS and other outside servicesSimple calculation classes such as FeeCalculator
Web clients calling other systemsJava 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

  1. Easy: Add a test that a student who is exactly 30 days late gets no notice, and one 31 days late does.
  2. Harder: Add a method billAll(List<String> names) to FeeService that 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 with times(1).

Quick check: Mockito

4 questions
1A test calls when(...).thenReturn(x) on repo.findByName("Navika"). What happens?
2What does an unstubbed mock method that returns Optional give back?
3How do you check that send was never called?
4Which of these is usually NOT worth mocking?

Key Takeaways

Was this tutorial helpful?

Lesson 530 of 578