Mocking with Mockito
Real classes rarely work alone. A checkout service calls a payment gateway and sends an SMS. A fee reminder reads students from a database. If a unit test used the real gateway, every test run would charge real money, need the internet, and fail whenever the bank's test server is down. Unit tests must be fast, repeatable and independent of the outside world.
The answer is a test double: a stand-in object that looks like the real dependency but is controlled by the test. Mockito is the most widely used library for creating them in Java. In this lesson you will create mocks, decide what they return, check how your code used them, and learn when not to mock. You should already know Unit Testing with JUnit.
Design for testing: depend on interfaces
Mocking works best when a class receives its dependencies through its constructor, typed as interfaces. This is called dependency injection, and it is exactly how Spring Boot applications are written. Here is the code we will test, in package in.shop:
public interface PaymentGateway {
/** Charges the customer and returns a payment reference, or throws PaymentFailedException. */
String charge(String customerId, BigDecimal amount);
}
public interface Notifier {
void send(String phone, String message);
}
public record Order(String id, String customerId, String phone, BigDecimal amount) {}
public class PaymentFailedException extends RuntimeException {
public PaymentFailedException(String message) {
super(message);
}
}
public class CheckoutService {
private static final BigDecimal FREE_DELIVERY_FROM = new BigDecimal("499");
private static final BigDecimal DELIVERY_FEE = new BigDecimal("40");
private final PaymentGateway gateway;
private final Notifier notifier;
public CheckoutService(PaymentGateway gateway, Notifier notifier) {
this.gateway = gateway;
this.notifier = notifier;
}
public String checkout(Order order) {
BigDecimal total = order.amount().compareTo(FREE_DELIVERY_FROM) >= 0
? order.amount()
: order.amount().add(DELIVERY_FEE);
try {
String ref = gateway.charge(order.customerId(), total);
notifier.send(order.phone(), "Order " + order.id() + " paid: Rs " + total + " (ref " + ref + ")");
return ref;
} catch (PaymentFailedException e) {
notifier.send(order.phone(), "Payment for order " + order.id() + " failed. No money was taken.");
throw e;
}
}
}(Each type goes in its own file with package in.shop; and import java.math.BigDecimal; where needed.) CheckoutService never creates a gateway itself; it is handed one. In production, that is a real gateway client. In a test, it is a mock.
To add Mockito with Maven, add org.mockito:mockito-junit-jupiter (version 5.24.0 at the time of writing) with test scope next to JUnit. It brings in mockito-core and the JUnit extension. Gradle uses testImplementation("org.mockito:mockito-junit-jupiter:5.24.0").
Stubbing and verifying
A mock is an object that records every call made to it and returns default values (null, 0, false, empty collections) unless you tell it otherwise. Telling it what to return is called stubbing: when(mock.method(args)).thenReturn(value). Checking afterwards that a call happened is called verifying: verify(mock).method(args).
package in.shop;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {
@Mock PaymentGateway gateway;
@Mock Notifier notifier;
@InjectMocks CheckoutService service;
@Captor ArgumentCaptor<String> message;
private final Order smallOrder = new Order("A-101", "C7", "98100-00001", new BigDecimal("299"));
@Test
void addsDeliveryFeeBelowRs499AndSendsSms() {
when(gateway.charge("C7", new BigDecimal("339"))).thenReturn("PAY-55");
String ref = service.checkout(smallOrder);
assertEquals("PAY-55", ref);
verify(notifier).send(eq("98100-00001"), message.capture());
assertEquals("Order A-101 paid: Rs 339 (ref PAY-55)", message.getValue());
}
@Test
void noDeliveryFeeFromRs499() {
Order big = new Order("A-102", "C7", "98100-00001", new BigDecimal("499"));
when(gateway.charge(anyString(), any())).thenReturn("PAY-56");
service.checkout(big);
verify(gateway).charge("C7", new BigDecimal("499"));
}
@Test
void failedPaymentTellsCustomerAndRethrows() {
when(gateway.charge(anyString(), any()))
.thenThrow(new PaymentFailedException("bank timeout"));
PaymentFailedException e = assertThrows(PaymentFailedException.class,
() -> service.checkout(smallOrder));
assertEquals("bank timeout", e.getMessage());
verify(notifier).send(eq("98100-00001"), contains("failed"));
verify(notifier, never()).send(anyString(), contains("paid"));
verifyNoMoreInteractions(notifier);
}
}What each piece does:
@ExtendWith(MockitoExtension.class)lets Mockito process the annotations before every test.@Mockcreates a fresh mock for each test.@InjectMockscreates the realCheckoutServiceand passes the mocks to its constructor.thenReturnmakes the gateway "succeed";thenThrowmakes it fail, so you can test the error path without breaking a real bank connection.- Argument matchers such as
anyString(),any(),eq(...)andcontains(...)match a range of values. If you use a matcher for one argument, you must use matchers for all arguments of that call, which is why the first test writeseq("98100-00001")and not just the string. verify(notifier, never())checks that something did not happen;times(2),atLeastOnce()andverifyNoMoreInteractionsgive finer control.- An
ArgumentCaptorgrabs the actual argument so you can make normal assertions about it, here the exact SMS text.
All three tests pass. Now suppose someone changes >= 0 to > 0, so an order of exactly Rs 499 is wrongly charged delivery. The console launcher reports:
│ └─ noDeliveryFeeFromRs499() ✘
│ Wanted but not invoked:
│ gateway.charge("C7", 499);
│ -> at in.shop.CheckoutServiceTest.noDeliveryFeeFromRs499(CheckoutServiceTest.java:51)
│
│ However, there was exactly 1 interaction with this mock:
│ gateway.charge("C7", 539);
│ -> at in.shop.CheckoutService.checkout(CheckoutService.java:22)Mockito shows both what the test expected and what really happened, with line numbers, so the Rs 40 bug is obvious.
Strict stubs keep tests honest
With MockitoExtension, Mockito uses strict stubs: a stub that your test sets up but the code never uses makes the test fail. This catches tests that no longer test what they claim, for example after a refactoring. A test that stubs the gateway but forgets to call checkout fails like this:
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 in.shop.StrictDemoTest.stubThatIsNeverUsed(StrictDemoTest.java:22) Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
Delete the unused stub rather than switching to lenient mode. A shared stub that only some tests use usually means the setup belongs in those tests.
Mocks, stubs and fakes: when not to mock
Mockito is a tool, not a goal. Some guidelines that experienced teams follow:
- Mock what you do not own and cannot run quickly: payment gateways, SMS and email senders, external HTTP APIs, the clock.
- Do not mock values. Records, strings,
BigDecimal, lists and maps are cheap to create; use real ones. Mockito's own documentation says not to mockList. - Do not mock the class under test. Mock its collaborators only.
- Prefer a simple fake for repositories when many tests need realistic behaviour. A fake is a small working implementation, such as an
InMemoryStudentRepositorybacked by aHashMap. It often makes tests shorter than dozens of stubs. - Too many mocks is a design smell. If a test needs six mocks, the class probably does too much; split it.
- Verify outcomes, not every call. Check what matters to the business (the customer was charged once, an SMS was sent) rather than every internal step, or each refactoring breaks the tests.
Database and HTTP code still needs testing against the real thing. That is the job of integration tests, which teams often run with a real database in a container (for example with Testcontainers) on the build server.
Common mistakes
- Mixing matchers and raw values in one call.
verify(n).send("98100-00001", anyString())throwsInvalidUseOfMatchersException; wrap the raw value ineq(...). - Creating the dependency inside the class.
new RealGateway()insideCheckoutServicecannot be replaced; pass it through the constructor. - Forgetting
@ExtendWith(MockitoExtension.class). The@Mockfields staynulland tests fail withNullPointerException. - Stubbing in every test "just in case". Strict stubs will fail the build; stub only what each test needs.
- Verifying that a stubbed call happened. If the return value is used in an assertion, the call obviously happened; verify only calls whose effect you cannot see otherwise, such as
send.
Key points
- Mocks replace slow or external dependencies so unit tests stay fast and repeatable.
- Pass dependencies in through the constructor, typed as interfaces, so tests can inject mocks.
@Mock,@InjectMocksand@ExtendWith(MockitoExtension.class)set everything up.when(...).thenReturn(...)andthenThrow(...)control behaviour;verify(...)withnever(),times(n)and captors checks interactions.- Strict stubs fail tests that set up stubs they never use.
- Do not mock values or the class under test; use fakes and integration tests where they fit better.
Exercise
A college has FeeReminderService, which receives a StudentRepository (method List<Student> findWithFeesDue()) and a Notifier (method void send(String phone, String message)). sendReminders() sends "Dear <name>, fees of Rs <amount> are due." to every student with fees due who has a non-blank phone number, and returns how many messages it sent. Write the service, the record Student(String roll, String name, String phone, int feesDue), and a Mockito test class with two tests: three students where one has an empty phone number (expect 2 messages, each checked exactly), and no students at all (expect 0 and no interaction with the notifier).
Show solution
package in.college;
public class FeeReminderService {
private final StudentRepository repository;
private final Notifier notifier;
public FeeReminderService(StudentRepository repository, Notifier notifier) {
this.repository = repository;
this.notifier = notifier;
}
/** Sends a reminder to every student with fees due who has a phone number; returns how many were sent. */
public int sendReminders() {
int sent = 0;
for (Student s : repository.findWithFeesDue()) {
if (s.phone() == null || s.phone().isBlank()) {
continue;
}
notifier.send(s.phone(), "Dear " + s.name() + ", fees of Rs " + s.feesDue() + " are due.");
sent++;
}
return sent;
}
}package in.college;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.util.List;
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;
@ExtendWith(MockitoExtension.class)
class FeeReminderServiceTest {
@Mock StudentRepository repository;
@Mock Notifier notifier;
@InjectMocks FeeReminderService service;
@Test
void remindsOnlyStudentsWithAPhoneNumber() {
when(repository.findWithFeesDue()).thenReturn(List.of(
new Student("R1", "Ananya", "98100-11111", 12000),
new Student("R2", "Farhan", "", 8500),
new Student("R3", "Isha", "98100-33333", 4000)));
int sent = service.sendReminders();
assertEquals(2, sent);
verify(notifier).send("98100-11111", "Dear Ananya, fees of Rs 12000 are due.");
verify(notifier).send("98100-33333", "Dear Isha, fees of Rs 4000 are due.");
verify(notifier, times(2)).send(anyString(), anyString());
}
@Test
void sendsNothingWhenNobodyOwesFees() {
when(repository.findWithFeesDue()).thenReturn(List.of());
assertEquals(0, service.sendReminders());
verifyNoInteractions(notifier);
}
}│ └─ FeeReminderServiceTest ✔ │ ├─ remindsOnlyStudentsWithAPhoneNumber() ✔ │ └─ sendsNothingWhenNobodyOwesFees() ✔
The StudentRepository and Notifier interfaces each have the single method named in the task. times(2) proves Farhan, who has no phone number, received nothing, and verifyNoInteractions proves an empty list sends no messages at all.
Quick Quiz
1. What does @InjectMocks do?
@InjectMocks builds the object you are testing, injecting the mocks through its constructor.
2. Which line makes a mocked gateway fail during a test?
thenThrow stubs a call so it throws, letting you test error handling.
3. Why does verify(notifier).send("98100-00001", anyString()) fail?
Mockito cannot mix raw values and matchers in one call.
4. Which should you usually NOT mock?
Value objects are cheap and predictable; create real ones.
Learn Java with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
