2.5 使用注解进行单元测试详解
Mockito 使用注解进行单元测试详解
Mockito 提供了一系列注解来简化测试代码的编写,减少手动创建和管理 Mock 对象的样板代码。结合 JUnit 5,可以更高效地构建清晰、易维护的单元测试。
1. 核心注解概览
注解 | 作用 |
---|---|
@Mock | 创建并注入一个 Mock 对象(完全模拟,方法默认返回空或默认值)。 |
@Spy | 创建并注入一个 Spy 对象(部分模拟,默认调用真实方法,除非显式覆盖)。 |
@InjectMocks | 自动将 @Mock 或 @Spy 对象注入到被测类中(依赖注入)。 |
@Captor | 自动初始化 ArgumentCaptor ,用于捕获方法参数。 |
@ExtendWith | 启用 Mockito 扩展(JUnit 5 必需),替代旧版 @RunWith 。 |
2. 注解配置与启用
2.1 启用 Mockito 支持
在测试类上添加 @ExtendWith(MockitoExtension.class)
,激活 Mockito 注解功能:
@ExtendWith(MockitoExtension.class) // JUnit 5 必加
public class UserServiceTest {
// 测试代码...
}
2.2 自动初始化注解
无需手动调用 MockitoAnnotations.openMocks(this)
,@ExtendWith
已自动处理。
3. 注解使用详解
3.1 @Mock
注解
作用:创建完全模拟的依赖对象。
示例场景:
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User getUserById(int id) {
return userDao.findById(id);
}
}
测试代码:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserDao mockUserDao; // 自动创建 Mock 对象
@InjectMocks
private UserService userService; // 自动注入 mockUserDao
@Test
void getUserById_ShouldReturnUser() {
// 配置 Mock 行为
when(mockUserDao.findById(1)).thenReturn(new User(1, "Alice"));
// 调用被测方法
User user = userService.getUserById(1);
// 验证结果
assertEquals("Alice", user.getName());
verify(mockUserDao).findById(1);
}
}
3.2 @Spy
注解
作用:创建部分模拟对象,保留真实方法逻辑,除非显式覆盖。
示例场景:
public class PaymentService {
public boolean validateCard(String cardNumber) {
return cardNumber != null && cardNumber.length() == 16;
}
public boolean processPayment(String cardNumber) {
if (!validateCard(cardNumber)) return false;
// 真实支付逻辑...
return true;
}
}
测试代码:
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Spy // 部分模拟,保留真实方法
private PaymentService spyPaymentService;
@Test
void processPayment_ShouldUseMockedValidation() {
// 覆盖 validateCard 方法
doReturn(true).when(spyPaymentService).validateCard(anyString());
// 调用被测方法(processPayment 会调用被覆盖的 validateCard)
boolean result = spyPaymentService.processPayment("invalid_card");
assertTrue(result);
verify(spyPaymentService).validateCard("invalid_card");
}
}
3.3 @InjectMocks
注解
作用:自动将 @Mock
或 @Spy
对象注入到被测类中。
注入规则:
- 构造器注入(优先):匹配参数类型和数量。
- Setter 注入:调用 setter 方法。
- 字段注入(最后):直接反射注入字段。
示例:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private InventoryService inventoryService;
@Mock
private PaymentService paymentService;
@InjectMocks // 自动注入 inventoryService 和 paymentService
private OrderService orderService;
@Test
void placeOrder_ShouldCheckInventory() {
when(inventoryService.checkStock(anyString())).thenReturn(true);
orderService.placeOrder("product_123");
verify(inventoryService).checkStock("product_123");
}
}
3.4 @Captor
注解
作用:自动创建参数捕获器,简化参数验证。
示例:
@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {
@Mock
private EmailClient mockEmailClient;
@InjectMocks
private NotificationService notificationService;
@Captor // 自动初始化 ArgumentCaptor
private ArgumentCaptor<EmailRequest> emailCaptor;
@Test
void sendWelcomeEmail_ShouldCaptureEmailContent() {
notificationService.sendWelcomeEmail("user@example.com");
verify(mockEmailClient).send(emailCaptor.capture());
EmailRequest captured = emailCaptor.getValue();
assertEquals("user@example.com", captured.getTo());
assertTrue(captured.getSubject().contains("Welcome"));
}
}
4. 常见问题与解决方案
问题 | 解决方案 |
---|---|
@Mock 对象为 null | 检查是否添加 @ExtendWith(MockitoExtension.class) 。 |
依赖注入失败 | 确保 @InjectMocks 类的依赖项有对应的 @Mock 或 @Spy 对象。 |
Spy 对象调用真实方法导致异常 | 使用 doReturn().when() 替代 when().thenReturn() 避免执行真实方法。 |
参数捕获器未初始化 | 使用 @Captor 替代手动创建 ArgumentCaptor 。 |
5. 高级整合:与 Spring Boot 测试结合
在 Spring Boot 测试中,可使用 @MockBean
替换容器中的 Bean:
@SpringBootTest
public class ProductServiceIntegrationTest {
@MockBean // Spring 管理的 Mock
private InventoryService mockInventoryService;
@Autowired
private ProductService productService;
@Test
void reserveProduct_ShouldUseMockInventory() {
when(mockInventoryService.reserve(anyString())).thenReturn(true);
boolean result = productService.reserveProduct("product_123");
assertTrue(result);
}
}
6. 最佳实践
- 保持测试简洁:使用注解减少手动初始化代码。
- 明确依赖关系:通过
@InjectMocks
明确被测类的依赖注入方式。 - 避免过度 Mock:仅 Mock 外部依赖,保留核心逻辑的真实性。
- 结合 AssertJ:使用流式断言提高测试可读性:
assertThat(capturedEmail.getSubject()).contains("Welcome");
通过合理使用 Mockito 注解,可以显著提升单元测试的编写效率和可维护性。