@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() 方法使用 HashMapOrder 对象保存在内存中:

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 定义。

使用 @SpyBeanNotificationService 添加一个集成测试:

@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