如何测试 Spring Application Event?

1、概览

本文将带你了解如何测试 Spring Application Event,以及如何使用 Spring Modulith 的测试库。

2、Application Event

Spring 提供了 Application Event(观察者设计模式),允许组件在保持松散耦合的同时相互通信。我们可以使用 ApplicationEventPublisher Bean 发布内部事件,这些事件都是纯 Java 对象,所有已注册的监听器(Listener)都会收到通知。

例如,当成功创建 Order 时,OrderService 组件可以发布 OrderCompletedEvent

@Service
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;
    
    // 构造函数

    public void placeOrder(String customerId, String... productIds) {
        Order order = new Order(customerId, Arrays.asList(productIds));
	// 验证和下单的业务逻辑

	OrderCompletedEvent event = new OrderCompletedEvent(savedOrder.id(), savedOrder.customerId(), savedOrder.timestamp());
	eventPublisher.publishEvent(event);
    }

}

OrderCompletedEvent 对象作为应用事件发布,不同模块中监听这些事件的组件会收到通知。

假设 LoyaltyPointsService 会对这些事件做出反应,以奖励下单客户积分。要实现这一点,可以使用 Spring 的 @EventListener 注解:

@Service
public class LoyaltyPointsService {

    private static final int ORDER_COMPLETED_POINTS = 60;

    private final LoyalCustomersRepository loyalCustomers;

    // 构造函数

    @EventListener
    public void onOrderCompleted(OrderCompletedEvent event) {
        // 奖励客户积分的业务逻辑
        loyalCustomers.awardPoints(event.customerId(), ORDER_COMPLETED_POINTS);
    }

}

使用 Application Event 而不是直接调用方法,能够保持较松的耦合,并反转两个模块之间的依赖关系。换句话说,“订单” 模块的源码并不依赖于 “奖励” 模块的类。

3、测试 Event Listener

我们可以通过在测试中发布 Application Event 来测试使用 @EventListener 的组件。

要测试 LoyaltyPointsService,需要创建 @SpringBootTest,注入 ApplicationEventPublisher Bean,并用它来发布 OrderCompletedEvent

@SpringBootTest
class EventListenerUnitTest {

    @Autowired
    private LoyalCustomersRepository customers;

    @Autowired
    private ApplicationEventPublisher testEventPublisher;

    @Test
    void whenPublishingOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints() {
        OrderCompletedEvent event = new OrderCompletedEvent("order-1", "customer-1", Instant.now());
        testEventPublisher.publishEvent(event);

	//  断言
    }

}

最后,需要断言 LoyaltyPointsService 消费了该事件,并向客户奖励了正确的积分。使用 LoyalCustomersRepository 来查看该客户获得了多少积分:

@Test
void whenPublishingOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints() {
    OrderCompletedEvent event = new OrderCompletedEvent("order-1", "customer-1", Instant.now());
    testEventPublisher.publishEvent(event);

    assertThat(customers.find("customer-1"))
      .isPresent().get()
      .hasFieldOrPropertyWithValue("customerId", "customer-1")
      .hasFieldOrPropertyWithValue("points", 60);
}

不出所料,测试通过了:“奖励” 模块接收并处理了事件,并发放了积分。

4、测试 Event Publisher

我们可以通过在测试包中创建自定义 Event Listener 来测试发布 Application Event 的组件。该 Listener 也使用 @EventHandler 注解,与上述实现类似。不过,这次我们将把所有传入的事件收集到一个列表中,并通过一个 getter 暴露出来:

@Component
class TestEventListener {

    final List<OrderCompletedEvent> events = new ArrayList<>();
    // Getter 方法

    @EventListener
    void onEvent(OrderCompletedEvent event) {
        events.add(event);
    }

    void reset() {
        events.clear();
    }
}

如你所见,还可以添加一个 reset() 工具方法。可以在每次测试前调用它,清除前一个测试产生的事件。

创建 Spring Boot 测试并通过 @Autowire 注入 TestEventListener 组件:

@SpringBootTest
class EventPublisherUnitTest {

    @Autowired
    OrderService orderService;

    @Autowired
    TestEventListener testEventListener;

    @BeforeEach
    void beforeEach() {
        testEventListener.reset();
    }

    @Test
    void whenPlacingOrder_thenPublishApplicationEvent() {
        // 下单

        assertThat(testEventListener.getEvents())
          // 检查发布的事件
    }

}

测试,使用 OrderService 组件下订单。之后,断言 testEventListener 接收到了恰好一个 Application Event,并具有适当的属性:

@Test
void whenPlacingOrder_thenPublishApplicationEvent() {
    orderService.placeOrder("customer1", "product1", "product2");

    assertThat(testEventListener.getEvents())
      .hasSize(1).first()
      .hasFieldOrPropertyWithValue("customerId", "customer1")
      .hasFieldOrProperty("orderId")
      .hasFieldOrProperty("timestamp");
}

如果你仔细观察,就会注意到这两个测试的设置和验证是相互补充的。这个测试模拟了方法调用并监听发布的事件,而前一个测试则发布事件并验证状态变化。换句话说,我们只使用了两个测试就测试了整个流程:每个测试覆盖了一个不同的部分,划分在逻辑模块的边界处。

5、Spring Modulith 的测试支持

Spring Modulith 提供了一系列可独立使用的工具库。这些库提供了一系列功能,主要目的是在应用的逻辑模块之间建立清晰的界限。

5.1、Scenario API

这种架构风格通过利用应用事件(Application Event)促进模块之间的灵活交互。因此,Spring Modulith 中的一个工具提供了对涉及 Application Event 的流程进行测试的支持。

pom.xml 中添加 spring-modulith-starter-test maven 依赖:

<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-test</artifactId>
    <version>1.1.3</version>
</dependency>

这样,就可以使用 Scenario API 以声明的方式编写测试。首先,创建一个测试类,并用 @ApplcationModuleTest 对其进行注解。这样,就能在任何测试方法中注入 Scenario 对象:

@ApplicationModuleTest
class SpringModulithScenarioApiUnitTest {
 
    @Test
    void test(Scenario scenario) {
        // ...
    }

}

简而言之,该功能提供了一个方便的 DSL,使我们能够测试最常见的用例。例如,它可以通过以下方式轻松启动测试并评估其结果:

  • 进行方法调用
  • 发布应用事件
  • 验证状态变化
  • 捕捉和验证传出事件

此外,API 还提供一些其他实用工具类方法,如

  • 轮询和等待异步应用事件
  • 定义超时
  • 对捕捉到的事件进行过滤和映射
  • 创建自定义断言

5.2、使用 Scenario API 测试 Event Listener

要测试使用 @EventListener 方法的组件,必须注入 ApplicationEventPublisher Bean 并发布 OrderCompletedEvent。不过,Spring Modulith 的测试 DSL 通过 scenario.publish() 提供了更直接的解决方案:

@Test
void whenReceivingPublishOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints(Scenario scenario) {
    scenario.publish(new OrderCompletedEvent("order-1", "customer-1", Instant.now()))
      .andWaitForStateChange(() -> loyalCustomers.find("customer-1"))
      .andVerify(it -> assertThat(it)
        .isPresent().get()
        .hasFieldOrPropertyWithValue("customerId", "customer-1")
        .hasFieldOrPropertyWithValue("points", 60));
}

方法 andWaitforStateChange() 接受一个 lambda 表达式,它会重复执行该表达式,直到返回一个非 null 对象或一个非空 Optional。这种机制对于异步方法调用特别有用。

最后,我们定义了一个场景:发布一个事件,等待状态变化,然后验证系统的最终状态。

5.3、使用 Scenario API 测试 Event Publisher

我们还可以使用 Scenario API 来模拟方法调用,拦截并验证传出的 Application Event。

使用 DSL 来编写一个测试,以验证 “order” 模块的行为:

@Test
void whenPlacingOrder_thenPublishOrderCompletedEvent(Scenario scenario) {
    scenario.stimulate(() -> orderService.placeOrder("customer-1", "product-1", "product-2"))
      .andWaitForEventOfType(OrderCompletedEvent.class)
      .toArriveAndVerify(evt -> assertThat(evt)
        .hasFieldOrPropertyWithValue("customerId", "customer-1")
        .hasFieldOrProperty("orderId")
        .hasFieldOrProperty("timestamp"));
}

如你所见,andWaitforEventOfType() 方法允许我们声明要捕获的事件类型。随后,toArriveAndVerify() 用于等待事件并执行相关断言。

6、总结

本文介绍了对 Spring Application Event 进行测试的各种方法。首先介绍了使用 ApplicationEventPublisher 手动发布 Application Event 进行测试,最后介绍了 Spring Modulith 的测试支持,并使用 Scenario API 以声明方式编写了相同的测试。Fluent 风格的 DSL 使我们能够发布和捕获 Application Event、模拟方法调用并等待状态变化。


Ref:https://www.baeldung.com/spring-test-application-events