@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