1. 引言
在这个教程中,我们将讨论@Spy
和@SpyBean
之间的差异,解释它们的功能,并指导何时使用每一种。
2. 基本应用
为了演示,我们使用一个简单的订单应用,它包括一个创建订单的OrderService
,以及在处理订单时调用的NotificationService
。
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
来接收警报,如果警报成功,它将返回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
注解
Mockito测试框架中的@Spy
注解用于创建一个真实对象的间谍(部分模拟),常用于单元测试。
间谍允许我们在跟踪特定方法的同时,选择性地重写或验证其行为,而其他方法仍然执行真实实现。
让我们通过为OrderService
编写一个单元测试,将NotificationService
标记为@Spy
来理解这一点:
@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
作为一个间谍对象,如果没有定义模拟,则会调用实际的notify()
方法。此外,由于我们为raiseAlert()
方法定义了模拟,NotificationService
就成为了一个部分模拟。
3.2. Spring Boot的@SpyBean
注解
另一方面,@SpyBean
注解是Spring Boot特有的,用于与Spring的依赖注入进行集成测试。
它允许我们在使用实际的Spring 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应用程序上下文管理NotificationService
并将其注入到OrderService
中。在NotificationService
内部调用notify()
会触发真实方法的执行,而调用raiseAlert()
则触发模拟的执行。
4. @Spy
和@SpyBean
之间的差异
让我们详细理解@Spy
和@SpyBean
之间的区别。
在单元测试中,我们使用@Spy
,而在集成测试中,我们使用@SpyBean
。
如果@Spy
注解的组件包含其他依赖项,我们可以在初始化期间声明它们。如果没有在初始化时提供,系统将使用可用的无参数构造函数。对于@SpyBean
测试,我们必须使用@Autowired
注解来注入依赖组件。否则,在运行时,Spring Boot会创建一个新的实例。
如果在单元测试示例中使用@SpyBean
,当NotificationService
被调用时,测试将因NullPointerException
而失败,因为OrderService
期望的是模拟/间谍NotificationService
。
同样,如果在集成测试示例中使用@Spy
,测试也将失败,因为错误消息将是'Wanted but not invoked: notificationService.notify(@Spy
注解的类。相反,它会创建NotificationService
的新实例并注入到OrderService
中。
5. 总结
在这篇文章中,我们探讨了@Spy
和@SpyBean
注解及其使用场景。
如往常一样,示例代码可以在GitHub上找到:GitHub链接。