@Spy 和 @SpyBean
1、简介
本文将带你了解 @Spy 和 @SpyBean 之间的区别和用法。
2、基本应用
本文中,我们使用一个简单的订单应用,其中包括一个用于创建订单的订单服务,以及一个用于在处理订单时发出通知的通知服务。
OrderService 有一个 save() 方法,用于接收 Order 对象,使用 OrderRepository 保存该对象,并调用 NotificationService:
@Service
public class OrderService {
public final OrderRepository orderRepository;
public final NotificationService notificationService;
public OrderService(OrderRepository orderRepository, NotificationService notificationService) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
}
public Order save(Order order) {
order = orderRepository.save(order);
notificationService.notify(order);
if(!notificationService.raiseAlert(order)){
throw new RuntimeException("Alert not raised");
}
return order;
}
}
为简单起见,我们假设 notify() 方法仅在日志中输出订单信息。实际上,它可能涉及更复杂的操作,例如通过队列向下游应用发送电子邮件或消息。
我们还假设,每个创建的订单都必须通过调用 ExternalAlertService 来接收警报(Alert),如果警报成功,则返回 true,如果 OrderService 没有发出警报,则会失败:
@Component
public class NotificationService {
private ExternalAlertService externalAlertService;
public void notify(Order order){
System.out.println(order);
}
public boolean raiseAlert(Order order){
return externalAlertService.alert(order);
}
}
OrderRepository 中的 save() 方法使用 HashMap 将 Order 对象保存在内存中:
public Order save(Order order) {
UUID orderId = UUID.randomUUID();
order.setId(orderId);
orders.put(UUID.randomUUID(), order);
return order;
}
3、@Spy 和 @SpyBean 注解的实际应用
现在我们有了一个基本的应用,让我们看看如何使用 @Spy 和 @SpyBean 注解来测试它的不同方面。
3.1、Mockito 的 @Spy 注解
@Spy 注解是 Mockito 测试框架的一部分,用于创建真实对象的 spy(部分模拟),常用于单元测试。
请注意,“mock” 和 “spy” 是 Mockito 框架中的术语,它们用于描述模拟对象的不同行为。“Mock” 对象是完全模拟的对象,它可以替代真实对象,并且可以设置其行为和验证方法的调用。而"Spy" 对象是部分模拟的对象,它可以保留真实对象的一部分行为,并且可以设置其余部分的行为和验证方法的调用。
Spy 允许我们跟踪并选择性地存根(stub)或验证真实对象的特定方法,同时仍然执行其他方法的真实实现。
让我们通过为 OrderService 编写一个单元测试来理解这个问题。
使用 @Spy 注解标记 NotificationService:
@Spy
OrderRepository orderRepository;
@Spy
NotificationService notificationService;
@InjectMocks
OrderService orderService;
@Test
void givenNotificationServiceIsUsingSpy_whenOrderServiceIsCalled_thenNotificationServiceSpyShouldBeInvoked() {
UUID orderId = UUID.randomUUID();
Order orderInput = new Order(orderId, "Test", 1.0, "17 St Andrews Croft, Leeds ,LS17 7TP");
doReturn(orderInput).when(orderRepository)
.save(any());
doReturn(true).when(notificationService)
.raiseAlert(any(Order.class));
Order order = orderService.save(orderInput);
Assertions.assertNotNull(order);
Assertions.assertEquals(orderId, order.getId());
verify(notificationService).notify(any(Order.class));
}
在这种情况下,NotificationService 充当了一个 spy 对象,当没有定义 mock 对象时,它会调用真实的 notify() 方法。此外,由于我们为 raiseAlert() 方法定义了一个 mock 对象,NotificationService 表现得像一个部分 mock 的对象。
3.2、Spring Boot 的 @SpyBean 注解
@SpyBean 注解是 Spring Boot 特有的,用于与 Spring 的依赖注入进行集成测试。
它允许我们创建 Spring Bean 的 Spy(部分模拟),同时仍然使用 Application Context 中的实际 Bean 定义。
使用 @SpyBean 为 NotificationService 添加一个集成测试:
@Autowired
OrderRepository orderRepository;
@SpyBean
NotificationService notificationService;
@SpyBean
OrderService orderService;
@Test
void givenNotificationServiceIsUsingSpyBean_whenOrderServiceIsCalled_thenNotificationServiceSpyBeanShouldBeInvoked() {
Order orderInput = new Order(null, "Test", 1.0, "17 St Andrews Croft, Leeds ,LS17 7TP");
doReturn(true).when(notificationService)
.raiseAlert(any(Order.class));
Order order = orderService.save(orderInput);
Assertions.assertNotNull(order);
Assertions.assertNotNull(order.getId());
verify(notificationService).notify(any(Order.class));
}
在这种情况下,Spring Application Context 管理 NotificationService 并将其注入 OrderService。在 NotificationService 中调用 notify() 会触发真实方法的执行,而调用 raiseAlert() 则会触发 mock 方法的执行。
4、@Spy 和 @SpyBean 的区别
@Spy 和 @SpyBean 之间的区别如下。
在单元测试中,使用 @Spy,而在集成测试中,使用 @SpyBean。
如果 @Spy 注解组件包含其他依赖项,我们可以在初始化时声明它们。如果在初始化过程中没有提供,系统将使用无参数构造函数(如果有的话)。在 @SpyBean 测试中,我们必须使用 @Autowired 注解来注入依赖组件。否则,在运行时,Spring Boot 会创建一个新实例。
如果在单元测试示例中使用 @SpyBean,那么当调用 NotificationService 时,测试就会失败,出现 NullPointerException 异常,因为 OrderService 期望使用 mock/spy NotificationService。
同样,如果在集成测试的示例中使用 @Spy,测试就会失败,显示错误信息 “Wanted but not invoked: notificationService.notify(<any com.baeldung.spytest.Order>”,因为 Spring Application Context 并不知道 @Spy 注解的类。相反,它会创建一个新的 NotificationService 实例并将其注入 OrderService。
参考:https://www.baeldung.com/spring-spy-vs-spybean