Mockito 单元测试框架实战
一、Mockito 简介
Mockito 是 Java 生态中最流行的 Mock 框架,被广泛应用于单元测试中。它提供了简洁优雅的 API,帮助开发者轻松创建和配置 Mock 对象,从而实现对依赖项的隔离测试。
1.1 为什么需要 Mock?
在实际开发中,我们常常遇到以下场景:
- 依赖的外部服务(数据库、HTTP API)难以在测试环境搭建
- 某些依赖执行缓慢,影响测试效率
- 需要模拟各种异常情况进行测试
- 希望独立测试某个模块,不受其他模块影响
Mockito 正是为解决这些问题而生,它可以创建虚拟对象替代真实依赖,让测试变得简单高效。
1.2 Mockito 的核心优势
- 简洁的 API:使用流畅的链式调用,代码可读性强
- 强大的验证功能:可以验证方法调用次数、参数等
- 灵活的参数匹配:支持精确匹配、类型匹配、自定义匹配
- 注解支持:通过注解简化 Mock 对象的创建
- 与主流框架集成:完美支持 JUnit、Spring Boot 等
二、Mockito 核心架构
Mockito 的架构分为四个主要层次:
- 测试代码层:我们编写的测试代码
- Mockito API:提供 mock()、when()、verify() 等核心方法
- Mock 对象代理:动态创建的代理对象
- 字节码增强:底层使用 ByteBuddy 进行字节码操作
三、快速开始
3.1 添加依赖
在项目中添加 Mockito 依赖:
<!-- Maven -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<!-- 如果使用 JUnit 5 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
// Gradle
testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.8.0'
3.2 基本用法三步曲
Mockito 的基本使用可以归纳为三个步骤:
步骤 1:创建 Mock 对象
import static org.mockito.Mockito.*;
// 方式一:使用 mock() 方法
List<String> mockList = mock(List.class);
// 方式二:使用 @Mock 注解
@Mock
private UserRepository userRepository;
步骤 2:定义 Mock 行为
// 定义返回值
when(mockList.get(0)).thenReturn("first");
when(mockList.size()).thenReturn(10);
// 定义异常
when(mockList.get(1)).thenThrow(new RuntimeException("出错了"));
// 链式调用
when(mockList.isEmpty())
.thenReturn(false)
.thenReturn(true); // 第二次调用返回 true
步骤 3:验证方法调用
// 验证方法被调用
verify(mockList).add("item");
// 验证调用次数
verify(mockList, times(2)).add(anyString());
// 验证从未调用
verify(mockList, never()).clear();
四、验证方法详解
Mockito 提供了丰富的验证方法来检查 Mock 对象的交互:
4.1 调用次数验证
import static org.mockito.Mockito.*;
@Test
public void testVerification() {
List<String> mockList = mock(List.class);
mockList.add("once");
mockList.add("twice");
mockList.add("twice");
// 验证调用 1 次(默认)
verify(mockList).add("once");
// 验证调用 2 次
verify(mockList, times(2)).add("twice");
// 验证从未调用
verify(mockList, never()).clear();
// 验证至少调用 1 次
verify(mockList, atLeastOnce()).add(anyString());
// 验证至少调用 2 次
verify(mockList, atLeast(2)).add("twice");
// 验证最多调用 5 次
verify(mockList, atMost(5)).add(anyString());
}
4.2 调用顺序验证
@Test
public void testInOrder() {
List<String> firstMock = mock(List.class);
List<String> secondMock = mock(List.class);
firstMock.add("first");
secondMock.add("second");
firstMock.add("third");
// 验证调用顺序
InOrder inOrder = inOrder(firstMock, secondMock);
inOrder.verify(firstMock).add("first");
inOrder.verify(secondMock).add("second");
inOrder.verify(firstMock).add("third");
}
4.3 超时验证
@Test
public void testTimeout() {
List<String> mockList = mock(List.class);
// 异步调用
new Thread(() -> {
try {
Thread.sleep(100);
mockList.add("async");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 验证在 200ms 内被调用
verify(mockList, timeout(200)).add("async");
}
五、参数匹配器
参数匹配器(ArgumentMatchers)让我们可以灵活地匹配方法参数。
5.1 常用匹配器
import static org.mockito.ArgumentMatchers.*;
@Test
public void testMatchers() {
Map<String, String> mockMap = mock(Map.class);
// any() - 匹配任何对象
when(mockMap.get(any())).thenReturn("value");
// anyString() - 匹配任何字符串
when(mockMap.put(anyString(), anyString())).thenReturn("old");
// anyInt() - 匹配任何整数
when(mockMap.remove(anyInt())).thenReturn("removed");
// eq() - 精确匹配
when(mockMap.get(eq("key"))).thenReturn("specific");
// isNull() - 匹配 null
when(mockMap.get(isNull())).thenReturn("null-value");
// isNotNull() - 匹配非 null
when(mockMap.containsKey(isNotNull())).thenReturn(true);
}
5.2 自定义匹配器
@Test
public void testCustomMatcher() {
List<String> mockList = mock(List.class);
// 使用 argThat 自定义匹配逻辑
when(mockList.add(argThat(s -> s.length() > 5)))
.thenReturn(true);
assertTrue(mockList.add("long string")); // true
assertFalse(mockList.add("short")); // false
}
5.3 混合使用注意事项
// ❌ 错误:不能混用匹配器和具体值
when(mockMap.put(anyString(), "value")).thenReturn("old");
// ✅ 正确:全部使用匹配器
when(mockMap.put(anyString(), eq("value"))).thenReturn("old");
六、高级特性
6.1 使用注解简化代码
Mockito 提供了多个注解来简化 Mock 对象的创建和注入:
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
// @Mock 创建 Mock 对象
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
// @InjectMocks 自动注入 Mock 依赖
@InjectMocks
private UserService userService;
@Test
public void testCreateUser() {
User user = new User("张三", "zhang@example.com");
when(userRepository.save(any(User.class))).thenReturn(user);
User created = userService.createUser(user);
verify(userRepository).save(user);
verify(emailService).sendWelcomeEmail(user.getEmail());
assertEquals("张三", created.getName());
}
}
6.2 Spy - 部分 Mock
Spy 允许我们监视真实对象,只 Mock 部分方法:
@Test
public void testSpy() {
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 真实方法会被调用
spyList.add("one");
spyList.add("two");
// Mock 特定方法
when(spyList.size()).thenReturn(100);
assertEquals(100, spyList.size()); // Mock 的行为
assertEquals("one", spyList.get(0)); // 真实的行为
}
实际应用场景:
public class OrderService {
public double calculateTotal(Order order) {
double subtotal = calculateSubtotal(order);
double tax = calculateTax(subtotal);
return subtotal + tax;
}
protected double calculateSubtotal(Order order) {
// 复杂的计算逻辑
return order.getItems().stream()
.mapToDouble(Item::getPrice)
.sum();
}
protected double calculateTax(double amount) {
return amount * 0.1;
}
}
@Test
public void testCalculateTotal() {
OrderService orderService = spy(new OrderService());
// 只 Mock calculateTax 方法
doReturn(50.0).when(orderService).calculateTax(anyDouble());
Order order = new Order();
order.addItem(new Item("商品", 100.0));
double total = orderService.calculateTotal(order);
assertEquals(150.0, total); // 100 + 50
}
6.3 Answer - 自定义返回逻辑
Answer 接口允许我们实现复杂的返回逻辑:
@Test
public void testAnswer() {
List<String> mockList = mock(List.class);
// 根据参数动态返回结果
when(mockList.get(anyInt())).thenAnswer(invocation -> {
Integer index = invocation.getArgument(0);
return "element-" + index;
});
assertEquals("element-0", mockList.get(0));
assertEquals("element-5", mockList.get(5));
}
实际应用场景 - 模拟异步回调:
public interface AsyncService {
void executeAsync(String task, Callback callback);
}
@Test
public void testAsyncCallback() {
AsyncService asyncService = mock(AsyncService.class);
doAnswer(invocation -> {
String task = invocation.getArgument(0);
Callback callback = invocation.getArgument(1);
// 模拟异步执行后调用回调
callback.onSuccess("完成: " + task);
return null;
}).when(asyncService).executeAsync(anyString(), any(Callback.class));
Callback mockCallback = mock(Callback.class);
asyncService.executeAsync("任务1", mockCallback);
verify(mockCallback).onSuccess("完成: 任务1");
}
6.4 ArgumentCaptor - 捕获参数
ArgumentCaptor 用于捕获传递给 Mock 方法的参数,便于后续断言:
@Test
public void testArgumentCaptor() {
List<String> mockList = mock(List.class);
mockList.add("John");
mockList.add("Jane");
// 创建 ArgumentCaptor
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
// 捕获所有调用的参数
verify(mockList, times(2)).add(captor.capture());
List<String> capturedValues = captor.getAllValues();
assertEquals(Arrays.asList("John", "Jane"), capturedValues);
}
实际应用场景 - 验证复杂对象:
@Test
public void testCaptureComplexObject() {
UserRepository userRepository = mock(UserRepository.class);
UserService userService = new UserService(userRepository);
userService.registerUser("张三", "zhang@example.com", 25);
// 捕获传递给 save 方法的 User 对象
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("张三", capturedUser.getName());
assertEquals("zhang@example.com", capturedUser.getEmail());
assertEquals(25, capturedUser.getAge());
assertNotNull(capturedUser.getCreatedAt());
}
七、Spring Boot 集成实战
在实际项目中,Mockito 最常见的应用场景是测试 Spring Boot 应用。
7.1 依赖配置
Spring Boot 项目通常已经包含 Mockito,但建议显式添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
7.2 Service 层测试
被测试的 Service:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final NotificationService notificationService;
@Autowired
public OrderService(OrderRepository orderRepository,
InventoryService inventoryService,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public Order createOrder(OrderRequest request) {
// 1. 检查库存
if (!inventoryService.checkStock(request.getProductId(), request.getQuantity())) {
throw new InsufficientStockException("库存不足");
}
// 2. 创建订单
Order order = new Order();
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
Order savedOrder = orderRepository.save(order);
// 3. 扣减库存
inventoryService.decreaseStock(request.getProductId(), request.getQuantity());
// 4. 处理支付
PaymentResult paymentResult = paymentService.processPayment(
savedOrder.getId(),
request.getPaymentMethod(),
calculateAmount(request)
);
if (paymentResult.isSuccess()) {
savedOrder.setStatus(OrderStatus.PAID);
orderRepository.save(savedOrder);
// 5. 发送通知
notificationService.sendOrderConfirmation(savedOrder);
} else {
// 回滚库存
inventoryService.increaseStock(request.getProductId(), request.getQuantity());
savedOrder.setStatus(OrderStatus.FAILED);
}
return savedOrder;
}
private BigDecimal calculateAmount(OrderRequest request) {
// 计算金额逻辑
return BigDecimal.valueOf(request.getQuantity() * 100);
}
}
测试类:
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private InventoryService inventoryService;
@Mock
private PaymentService paymentService;
@Mock
private NotificationService notificationService;
@InjectMocks
private OrderService orderService;
private OrderRequest orderRequest;
@BeforeEach
void setUp() {
orderRequest = new OrderRequest();
orderRequest.setProductId(1L);
orderRequest.setQuantity(2);
orderRequest.setUserId(100L);
orderRequest.setPaymentMethod("CREDIT_CARD");
}
@Test
@DisplayName("成功创建订单")
void testCreateOrder_Success() {
// Given
when(inventoryService.checkStock(1L, 2)).thenReturn(true);
Order mockOrder = new Order();
mockOrder.setId(1L);
mockOrder.setStatus(OrderStatus.PENDING);
when(orderRepository.save(any(Order.class))).thenReturn(mockOrder);
PaymentResult paymentResult = new PaymentResult(true, "SUCCESS");
when(paymentService.processPayment(anyLong(), anyString(), any(BigDecimal.class)))
.thenReturn(paymentResult);
// When
Order result = orderService.createOrder(orderRequest);
// Then
assertNotNull(result);
assertEquals(OrderStatus.PAID, result.getStatus());
// 验证交互
verify(inventoryService).checkStock(1L, 2);
verify(inventoryService).decreaseStock(1L, 2);
verify(orderRepository, times(2)).save(any(Order.class));
verify(paymentService).processPayment(eq(1L), eq("CREDIT_CARD"), any(BigDecimal.class));
verify(notificationService).sendOrderConfirmation(result);
}
@Test
@DisplayName("库存不足时抛出异常")
void testCreateOrder_InsufficientStock() {
// Given
when(inventoryService.checkStock(1L, 2)).thenReturn(false);
// When & Then
assertThrows(InsufficientStockException.class, () -> {
orderService.createOrder(orderRequest);
});
// 验证不会调用后续方法
verify(inventoryService).checkStock(1L, 2);
verify(inventoryService, never()).decreaseStock(anyLong(), anyInt());
verify(orderRepository, never()).save(any(Order.class));
verify(paymentService, never()).processPayment(anyLong(), anyString(), any(BigDecimal.class));
}
@Test
@DisplayName("支付失败时回滚库存")
void testCreateOrder_PaymentFailed() {
// Given
when(inventoryService.checkStock(1L, 2)).thenReturn(true);
Order mockOrder = new Order();
mockOrder.setId(1L);
when(orderRepository.save(any(Order.class))).thenReturn(mockOrder);
PaymentResult paymentResult = new PaymentResult(false, "INSUFFICIENT_FUNDS");
when(paymentService.processPayment(anyLong(), anyString(), any(BigDecimal.class)))
.thenReturn(paymentResult);
// When
Order result = orderService.createOrder(orderRequest);
// Then
assertEquals(OrderStatus.FAILED, result.getStatus());
// 验证回滚库存
verify(inventoryService).decreaseStock(1L, 2);
verify(inventoryService).increaseStock(1L, 2);
verify(notificationService, never()).sendOrderConfirmation(any(Order.class));
}
}
7.3 Controller 层测试
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(new OrderResponse(order));
}
}
// 测试类
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
@DisplayName("POST /api/orders - 成功创建订单")
void testCreateOrder() throws Exception {
// Given
OrderRequest request = new OrderRequest();
request.setProductId(1L);
request.setQuantity(2);
Order mockOrder = new Order();
mockOrder.setId(1L);
mockOrder.setStatus(OrderStatus.PAID);
when(orderService.createOrder(any(OrderRequest.class))).thenReturn(mockOrder);
// When & Then
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":1,\"quantity\":2}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.status").value("PAID"));
verify(orderService).createOrder(any(OrderRequest.class));
}
}
八、最佳实践
8.1 优先 Mock 接口而非具体类
// ✅ 推荐:Mock 接口
UserRepository userRepository = mock(UserRepository.class);
// ❌ 不推荐:Mock 具体类(可能遇到 final 方法问题)
UserRepositoryImpl userRepository = mock(UserRepositoryImpl.class);
8.2 避免过度 Mock
// ❌ 过度 Mock - 测试变得脆弱
@Test
void badTest() {
when(dependency1.method1()).thenReturn("value1");
when(dependency2.method2()).thenReturn("value2");
when(dependency3.method3()).thenReturn("value3");
// ... 大量的 Mock 设置
// 实际业务逻辑被淹没
service.doSomething();
// 验证大量细节
verify(dependency1, times(2)).method1();
verify(dependency2).method2();
// ...
}
// ✅ 适度 Mock - 只关注核心交互
@Test
void goodTest() {
// 只 Mock 必要的依赖
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
User result = userService.getUser(1L);
assertEquals("张三", result.getName());
verify(userRepository).findById(1L);
}
8.3 使用 @InjectMocks 自动注入
// ✅ 推荐:使用注解自动注入
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
// ❌ 不推荐:手动创建(容易出错且繁琐)
UserService userService = new UserService(userRepository, emailService, ...);
8.4 合理使用 ArgumentCaptor
// ✅ 推荐:验证复杂对象的属性
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
assertEquals("张三", captor.getValue().getName());
// ❌ 不推荐:简单参数使用 Captor(直接匹配即可)
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(service).process(captor.capture());
assertEquals("value", captor.getValue()); // 直接用 eq("value") 更简洁
8.5 验证重要交互,忽略次要细节
// ✅ 推荐:只验证核心业务逻辑
@Test
void testUserRegistration() {
userService.register(user);
verify(userRepository).save(user); // 核心:保存用户
verify(emailService).sendWelcome(user.getEmail()); // 核心:发送邮件
// 不验证日志记录等次要操作
}
// ❌ 不推荐:验证过多细节
verify(logger).info("开始注册用户");
verify(validator).validate(user);
verify(logger).debug("用户验证通过");
// ... 测试变得脆弱
8.6 使用有意义的测试数据
// ✅ 推荐:清晰的测试数据
User testUser = new User("张三", "zhang@example.com", 25);
// ❌ 不推荐:无意义的数据
User testUser = new User("test", "test@test.com", 0);
8.7 一个测试方法只测一个场景
// ✅ 推荐:分离测试场景
@Test
void testCreateUser_Success() {
// 测试成功场景
}
@Test
void testCreateUser_DuplicateEmail() {
// 测试邮箱重复场景
}
@Test
void testCreateUser_InvalidInput() {
// 测试无效输入场景
}
// ❌ 不推荐:在一个测试中测试多个场景
@Test
void testCreateUser() {
// 成功场景
// 失败场景
// 异常场景
// ... 难以定位问题
}
九、总结
Mockito 是 Java 单元测试的利器,掌握它可以显著提升测试质量和开发效率。 希望这篇文章能帮助你更好地使用 Mockito,写出高质量的单元测试!