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);
    }

}

OrderRepositorysave()方法使用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()',因为Spring应用程序上下文不知道带有@Spy注解的类。相反,它会创建NotificationService的新实例并注入到OrderService中。

5. 总结

在这篇文章中,我们探讨了@Spy@SpyBean注解及其使用场景。

如往常一样,示例代码可以在GitHub上找到:GitHub链接