Mockito 深度详解与完整代码实战
一、Mockito 核心概念
Mockito 是 Java 最流行的模拟框架,用于单元测试中创建测试替身(Test Doubles)。它通过模拟依赖对象的行为,使单元测试更加隔离和可控。
核心对象类型:
二、环境配置(Maven)
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
三、基础使用:Mock 与 Stubbing
1. 创建 Mock 对象
import static org.mockito.Mockito.*;
// 方式1:静态方法
List<String> mockedList = mock(List.class);
// 方式2:注解(需配合扩展)
@ExtendWith(MockitoExtension.class)
class MockitoTest {
@Mock
List<String> mockedList;
}
2. 方法行为设置(Stubbing)
// 基本Stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");
when(mockedList.contains(any(String.class))).thenReturn(true);
// 连续Stubbing
when(mockedList.size())
.thenReturn(1)
.thenReturn(2)
.thenThrow(new IllegalStateException());
// void方法Stubbing
doThrow(RuntimeException.class).when(mockedList).clear();
四、行为验证
// 基本验证
mockedList.add("one");
verify(mockedList).add("one");
// 验证调用次数
verify(mockedList, times(1)).add("one");
verify(mockedList, never()).add("two");
verify(mockedList, atLeast(2)).size();
// 验证调用顺序
InOrder inOrder = inOrder(mockedList);
inOrder.verify(mockedList).add("one");
inOrder.verify(mockedList).clear();
// 超时验证
verify(mockedList, timeout(100).times(1)).get(0);
五、参数捕获器
@Mock
UserRepository userRepository;
@Captor
ArgumentCaptor<User> userCaptor;
@Test
void testUserRegistration() {
UserService service = new UserService(userRepository);
User newUser = new User("john@example.com", "John");
service.registerUser(newUser);
verify(userRepository).save(userCaptor.capture());
User savedUser = userCaptor.getValue();
assertEquals("John", savedUser.getName());
assertNotNull(savedUser.getRegistrationDate());
}
六、Spy:部分真实对象
// 创建Spy对象(包装真实实例)
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 默认调用真实方法
spyList.add("real");
spyList.add("data");
// 部分方法模拟
doReturn(100).when(spyList).size();
// 混合行为
assertEquals("real", spyList.get(0)); // 真实方法
assertEquals(100, spyList.size()); // 模拟方法
七、Mockito 与依赖注入
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
PaymentGateway paymentGateway;
@Mock
InventoryService inventoryService;
@InjectMocks
OrderService orderService; // 自动注入mocks
@Test
void placeOrderSuccess() {
// 设置行为
when(inventoryService.isInStock("A001")).thenReturn(true);
when(paymentGateway.process(anyDouble())).thenReturn(true);
// 执行测试
Order order = new Order("A001", 2, 50.0);
OrderResult result = orderService.placeOrder(order);
// 验证结果
assertTrue(result.isSuccess());
verify(inventoryService).deductStock("A001", 2);
}
}
八、高级特性
1. 自定义应答(Answer)
when(mockedList.get(anyInt())).thenAnswer(invocation -> {
int index = invocation.getArgument(0);
return "Element at " + index;
});
assertEquals("Element at 5", mockedList.get(5));
2. 静态方法模拟(Mockito 4.x+)
try (MockedStatic<Utility> mocked = mockStatic(Utility.class)) {
mocked.when(Utility::generateId).thenReturn("fixed-id");
assertEquals("fixed-id", Utility.generateId());
mocked.verify(Utility::generateId);
}
3. BDD 风格(Given-When-Then)
import static org.mockito.BDDMockito.*;
@Test
void bddStyleTest() {
// Given
given(userRepository.findById(1L)).willReturn(Optional.of(new User(1L, "Alice")));
// When
User user = userService.getUserById(1L);
// Then
then(userRepository).should().findById(1L);
assertEquals("Alice", user.getName());
}
九、完整业务场景测试示例
业务类
public class PaymentProcessor {
private final PaymentGateway gateway;
private final FraudDetectionService fraudDetection;
public PaymentProcessor(PaymentGateway gateway, FraudDetectionService fraudDetection) {
this.gateway = gateway;
this.fraudDetection = fraudDetection;
}
public PaymentResult process(PaymentRequest request) {
if (fraudDetection.isSuspicious(request)) {
return PaymentResult.fraudDetected();
}
boolean success = gateway.processPayment(
request.getAmount(),
request.getCurrency(),
request.getCardDetails()
);
return success ?
PaymentResult.success() :
PaymentResult.failed("Payment failed");
}
}
测试类
@ExtendWith(MockitoExtension.class)
class PaymentProcessorTest {
@Mock PaymentGateway paymentGateway;
@Mock FraudDetectionService fraudDetection;
@InjectMocks PaymentProcessor processor;
@Test
void processPaymentSuccess() {
// Given
PaymentRequest request = new PaymentRequest(100.0, "USD", "4111111111111111");
given(fraudDetection.isSuspicious(request)).willReturn(false);
given(paymentGateway.processPayment(100.0, "USD", "4111111111111111"))
.willReturn(true);
// When
PaymentResult result = processor.process(request);
// Then
assertTrue(result.isSuccess());
then(paymentGateway).should().processPayment(100.0, "USD", "4111111111111111");
}
@Test
void fraudDetectionBlocksPayment() {
// Given
PaymentRequest request = new PaymentRequest(5000.0, "USD", "1234567890123456");
given(fraudDetection.isSuspicious(request)).willReturn(true);
// When
PaymentResult result = processor.process(request);
// Then
assertTrue(result.isFraud());
then(paymentGateway).should(never()).processPayment(anyDouble(), anyString(), anyString());
}
@Test
void paymentGatewayFailure() {
// Given
PaymentRequest request = new PaymentRequest(50.0, "EUR", "5555555555554444");
given(fraudDetection.isSuspicious(request)).willReturn(false);
given(paymentGateway.processPayment(50.0, "EUR", "5555555555554444"))
.willReturn(false);
// When
PaymentResult result = processor.process(request);
// Then
assertFalse(result.isSuccess());
assertEquals("Payment failed", result.getErrorMessage());
}
}
十、Mockito 最佳实践
1. 测试结构(AAA 模式)
@Test
void testStructure() {
// Arrange: 准备测试数据和行为
when(repository.findByName("test")).thenReturn(new Entity());
// Act: 执行被测方法
Result result = service.execute("test");
// Assert: 验证结果和行为
assertNotNull(result);
verify(repository).findByName("test");
}
2. 避免过度使用 Mocks
- 优先测试真实对象:对值对象、工具类等使用真实实例
- 不要 Mock 不可控类型:如集合类、日期类
- 避免 Mock 第三方库:使用测试替身或容器化测试
3. 合理使用 ArgumentMatchers
// 精确匹配
verify(repo).findById(eq(1L));
// 灵活匹配
verify(repo).save(any(User.class));
verify(repo).delete(argThat(user -> user.getName().startsWith("A")));
4. 测试异常场景
@Test
void testException() {
// 设置异常行为
when(service.dangerousOperation()).thenThrow(new IllegalStateException());
// 验证异常
assertThrows(IllegalStateException.class, () -> {
controller.handleRequest();
});
// 验证异常后的清理操作
verify(cleanupService).execute();
}
十一、常见问题解决方案
问题 | 解决方案 |
---|---|
NPE 未初始化 Mock | 添加 @ExtendWith(MockitoExtension.class) |
when() 调用真实方法 | 使用 doReturn().when() 替代 |
泛型类型警告 | 使用 @Mock 代替 mock() 或添加 @SuppressWarnings |
静态方法模拟失败 | 确保使用 MockedStatic 和 try-with-resources |
验证参数为 null | 使用 isNull() 匹配器 |
多次调用返回相同值 | 使用 thenReturn 链式调用 |
十二、Mockito 架构原理
十三、Mockito 与 Spring Boot 集成
@SpringBootTest
class IntegrationTest {
@MockBean
private ExternalService externalService; // Spring管理的Mock
@Autowired
private MainService mainService;
@Test
void testIntegration() {
given(externalService.callApi()).willReturn("mocked");
String result = mainService.execute();
assertEquals("processed: mocked", result);
}
}
十四、高级场景:深度模拟
@Test
void testDeepMock() {
OrderService orderService = mock(OrderService.class);
Order order = mock(Order.class);
// 深度Stubbing
when(orderService.getOrder(anyLong())).thenReturn(order);
when(order.getItems()).thenReturn(Arrays.asList("Item1", "Item2"));
// 执行测试
Order retrieved = orderService.getOrder(1L);
List<String> items = retrieved.getItems();
// 验证
assertEquals(2, items.size());
verify(order).getItems();
}
十五、Mockito 性能优化
- 重用 Mock 对象:在
@BeforeEach
中初始化 - 轻量级测试:避免复杂逻辑在 Stubbing 中
- 限制 Mock 数量:每个测试不超过 3-5 个 Mock
- 使用
@Mock
替代mock()
:减少样板代码
Mockito 核心价值:
- 测试隔离:切断外部依赖,专注单元逻辑
- 行为验证:确保对象间正确交互
- 异常模拟:轻松测试错误处理路径
- 快速反馈:无需启动整个应用上下文
最佳实践组合:
- 单元测试:Mockito + JUnit5
- 集成测试:Mockito + SpringBootTest + Testcontainers
- 行为验证:Mockito + AssertJ
- 覆盖率:Mockito + JaCoCo
完整项目结构示例:
src/
├── main/
│ └── java/
│ └── com/
│ └── example/
│ ├── service/
│ ├── repository/
│ └── model/
└── test/
└── java/
└── com/
└── example/
├── unit/ # 纯Mockito测试
├── integration/ # 集成测试
└── e2e/ # 端到端测试