In the previous article we learned how to write unit tests with JUnit 5. But reality is about to hit you hard — the Service you wrote depends on a DAO, and the DAO depends on a database; the Controller you wrote depends on an HTTP request object; the business logic you wrote depends on a third-party API. Do you really have to start a database, reach out to the internet and stand up microservices every time you run a test? Mockito tells you: no. Mock these dependencies and your tests will run like lightning.
The Problem: Why You Can’t Test It Directly
Take a typical scenario:
public class OrderService {
private PaymentGateway paymentGateway; // depends on a third-party payment API
private OrderRepository repository; // depends on the database
private EmailService emailService; // depends on the mail service
public OrderResult placeOrder(Order order) {
// 1. Check stock (query the database)
if (!repository.hasStock(order)) {
throw new OutOfStockException();
}
// 2. Charge the payment (call the third-party payment service)
PaymentResult payment = paymentGateway.charge(order.getAmount());
// 3. Save the order (write to the database)
repository.save(order);
// 4. Send the email notification (call the mail service)
emailService.sendConfirmation(order.getEmail());
return OrderResult.success(order.getId());
}
}
The problems with testing placeOrder directly:
- It needs a real database
- It needs a real payment gateway (charging money on every test run?)
- It needs a real mail server
- The test is slow (seconds turn into minutes)
- The test is unstable (the network goes down and the test goes down with it, even though the code is fine)
- Exceptional scenarios are hard to simulate (how do you test a payment timeout or insufficient stock?)
What Mockito Is
Mockito is the most popular mocking framework in the Java ecosystem. Its core is not “writing tests for you” but faking dependencies for you.
Rewriting the test above with Mockito:
@Test
void shouldPlaceOrderSuccessfully() {
// 1. Create mock objects (fake dependencies)
PaymentGateway mockPayment = mock(PaymentGateway.class);
OrderRepository mockRepo = mock(OrderRepository.class);
EmailService mockEmail = mock(EmailService.class);
// 2. Set up the mock behavior
when(mockRepo.hasStock(any())).thenReturn(true); // always in stock
when(mockPayment.charge(any())).thenReturn(PaymentResult.ok()); // payment always succeeds
// 3. Inject the mocks and run the test
OrderService service = new OrderService(mockPayment, mockRepo, mockEmail);
OrderResult result = service.placeOrder(new Order());
// 4. Assert the result
assertEquals(OrderStatus.SUCCESS, result.getStatus());
// 5. Verify: confirm that save and sendConfirmation were called
verify(mockRepo).save(any(Order.class));
verify(mockEmail).sendConfirmation(anyString());
}
Nowhere in that whole process do we connect to any database, payment gateway or mail service. Everything runs in memory and finishes in milliseconds.
Mockito’s three core capabilities:
| Capability | Meaning |
|---|---|
| Stubbing | Sets what value a mock object returns |
| Verification | Checks whether a method was called, and how many times |
| Mock | Creates a fake object to stand in for a real dependency |
Quick Start
Maven Dependency
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 needs an extra integration module -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
Creating Mock Objects
Mockito gives you three ways:
// Way 1: create it manually
OrderRepository mockRepo = mock(OrderRepository.class);
// Way 2: create it with an annotation (recommended)
@Mock
OrderRepository mockRepo;
// Way 3: JUnit 5 integration (most recommended)
@ExtendWith(MockitoExtension.class) // add this to the class
class OrderServiceTest {
@Mock
OrderRepository mockRepo;
@Test
void test() {
// mockRepo has already been initialized for you
}
}
With the annotation approach you don’t need to call initMocks(this) inside @BeforeEach — MockitoExtension handles it for you.
Stubbing
Stubbing means telling a mock object: “when someone calls this method, return this value”.
Basic Stubbing
// return a fixed value
when(mockRepo.findById(1L)).thenReturn(Optional.of(new Order()));
// return several values, used in turn
when(mockRepo.count()).thenReturn(1L, 2L, 3L);
// the 1st call returns 1, the 2nd returns 2, the 3rd returns 3
// throw an exception
when(mockPayment.charge(any())).thenThrow(new PaymentTimeoutException());
// use real logic
when(mockRepo.save(any())).thenAnswer(invocation -> {
Order order = invocation.getArgument(0);
order.setId(System.currentTimeMillis()); // simulate an auto-increment ID
return order;
});
Argument Matchers
You don’t always return the same value on every call — you need to return different things depending on the arguments:
// any() matches any argument
when(mockRepo.findById(anyLong())).thenReturn(Optional.empty());
// exact matching
when(mockRepo.findById(1L)).thenReturn(Optional.of(order1));
when(mockRepo.findById(2L)).thenReturn(Optional.of(order2));
// eq() pins an exact value when you mix it with other matchers
when(mockService.process(eq("VIP"), anyInt())).thenReturn(true);
// custom matcher
when(mockRepo.find(argThat(order -> order.getAmount() > 100)))
.thenReturn(Collections.singletonList(bigOrder));
Important: once you use a matcher like any(), every argument must use a matcher. You can’t mix them:
// wrong! this throws an exception
when(mockService.process("VIP", anyInt()));
// correct
when(mockService.process(eq("VIP"), anyInt()));
Verification
Stubbing sets “what it should return”; verification confirms “that it really was called”.
Basic Verification
// confirm that a method was called once
verify(mockRepo).save(any(Order.class));
// confirm that it was called N times
verify(mockRepo, times(3)).findById(anyLong());
// confirm that it was never called
verify(mockEmail, never()).sendConfirmation(anyString());
// confirm at least / at most
verify(mockRepo, atLeast(1)).save(any());
verify(mockRepo, atMost(5)).save(any());
Verifying the Call Order
InOrder inOrder = inOrder(mockRepo, mockEmail);
inOrder.verify(mockRepo).save(any()); // save first
inOrder.verify(mockEmail).sendConfirmation(); // then send the email
If the actual call order is reversed, the test fails.
Verifying There Are No Other Interactions
verify(mockRepo).save(any());
verifyNoMoreInteractions(mockRepo); // confirm that save is the only method that was called
This one is strict — if there is any other pointless call, the test fails. It suits strict verification of a critical path.
Spy (Partial Mocking)
Sometimes you don’t want to fake an entire object; you just want to “track calls on a real object while overriding a few individual methods”:
// a real List
List<String> list = new ArrayList<>();
// wrap it as a spy
List<String> spyList = spy(list);
// most methods use the real logic; only a few are overridden
when(spyList.size()).thenReturn(100); // size() returns 100 (fake)
spyList.add("hello"); // add() uses the real logic (it really is added)
assertEquals(100, spyList.size());
assertEquals("hello", spyList.get(0)); // get() is real logic too
Use Spy more cautiously than Mock. If you use Spy rather than Mock in large amounts, that’s a sign the object under test may need refactoring — its dependencies run too deep to mock.
Patterns in Practice
Pattern 1: Verifying the Exception Path
@Test
@DisplayName("库存不足时,应抛出 OutOfStockException 且不扣款不发邮件")
void shouldThrowWhenOutOfStock() {
when(mockRepo.hasStock(any())).thenReturn(false);
assertThrows(OutOfStockException.class,
() -> service.placeOrder(new Order()));
verify(mockPayment, never()).charge(any()); // no charge
verify(mockEmail, never()).sendConfirmation(); // no email sent
}
Pattern 2: Verifying the Return Value Is Correct
@Test
@DisplayName("VIP 用户享受 9 折优惠")
void shouldApplyDiscountForVIP() {
User vipUser = new User("vip001", UserType.VIP);
when(mockUserService.getCurrentUser()).thenReturn(vipUser);
Price finalPrice = pricingService.calculate(order);
assertEquals(new BigDecimal("90.0"), finalPrice.getAmount());
}
Pattern 3: Verifying That a Side Effect Happened
@Test
@DisplayName("注册成功后应发送欢迎邮件")
void shouldSendWelcomeEmailAfterRegistration() {
when(mockUserRepo.save(any())).thenReturn(newUser);
registrationService.register("newuser", "password");
ArgumentCaptor<Email> captor = ArgumentCaptor.forClass(Email.class);
verify(mockEmailService).send(captor.capture());
Email sent = captor.getValue();
assertEquals("newuser@example.com", sent.getTo());
assertTrue(sent.getBody().contains("欢迎"));
}
ArgumentCaptor captures the actual arguments passed to a mock method — you can make finer-grained assertions against them.
Common Pitfalls
1. Don’t Mock Value Objects
// wrong: mock a POJO
User mockUser = mock(User.class);
when(mockUser.getName()).thenReturn("张三");
// correct: just use new
User user = new User("张三", 25);
Value objects (POJOs, DTOs) have no external dependencies and don’t need mocking. Mock should be used for external dependencies with side effects (databases, networks, file systems).
2. Don’t Mock Things You Can’t Control
Don’t mock JDK built-ins like System.currentTimeMillis() or new Date(). If you need to control time, inject a Clock object:
public class OrderService {
private Clock clock; // injectable, mockable
}
// in the test
when(mockClock.instant()).thenReturn(Instant.parse("2018-09-01T10:00:00Z"));
3. Don’t Forget to Verify
Stubbing without verifying is only checking the “happy path”; you have no idea whether the exception path was ever triggered:
// not good enough
when(mockRepo.hasStock(any())).thenReturn(false);
assertThrows(OutOfStockException.class, () -> service.placeOrder(order));
// better — add verification
verify(mockPayment, never()).charge(any()); // confirm that no charge was made
4. Don’t Mock Types
Mockito can’t mock final classes, final methods or static methods (at least not in 2.x). If you run into a dependency like that, the design is too tightly coupled and you should consider introducing an interface to decouple it.
The Relationship Between Mockito and JUnit 5
Mockito doesn’t replace JUnit, and JUnit doesn’t replace Mockito — they’re partners:
JUnit 5 负责:跑测试、断言结果、管理生命周期
Mockito 负责:创建 mock、打桩、验证交互
A typical test class structure:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository mockRepo;
@Mock PaymentGateway mockPayment;
@Mock EmailService mockEmail;
@InjectMocks // automatically inject the mocks into the service's constructor
OrderService service;
@Test
void test() {
// 1. Stub
when(mockRepo.hasStock(any())).thenReturn(true);
when(mockPayment.charge(any())).thenReturn(PaymentResult.ok());
// 2. Execute
OrderResult result = service.placeOrder(new Order());
// 3. Assert
assertEquals(OrderStatus.SUCCESS, result.getStatus());
// 4. Verify
verify(mockRepo).save(any());
}
}
@InjectMocks automatically injects the mocks annotated with @Mock into the object annotated with @InjectMocks — through the constructor, a setter, or field injection.
Summary
The core problem Mockito solves is: freeing unit tests from external dependencies.
| Without Mockito | With Mockito |
|---|---|
| Tests must connect to a database, slow | Runs in memory, millisecond-level |
| Tests must reach the network, unstable | No network dependency at all |
| Exception scenarios are hard to construct | thenThrow() handles it in one line |
| Test results are unpredictable | Stubbing controls everything |
But it isn’t a cure-all — Mockito is for isolation at the unit test layer; if an integration test needs a database, it should still connect to one. Only by recognizing its boundaries can you put it to good use.
Starting with the next post we’ll turn to another topic: with all these tests written, how much code do you actually cover? JaCoCo will give you the answer.
A small step forward every day is a new height!