1. 概述

本文将探讨如何测试使用Spring应用事件的代码。首先,我们会手动创建测试工具类来发布和收集应用事件,用于测试目的。之后,我们将介绍Spring Modulith的测试库,并使用其流式Scenario API来讨论常见测试用例。通过这种声明式DSL,我们能编写出可轻松生成和消费应用事件的测试用例。

2. 应用事件

Spring框架提供应用事件机制,允许组件间保持松耦合的同时进行通信。我们可以使用ApplicationEventPublisher bean来发布内部事件(这些事件就是普通Java对象),所有注册的监听器都会收到通知。

例如,当订单成功创建时,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);
    }
}

如你所见,完成的订单现在作为应用事件发布。因此,不同模块的组件可以监听这些事件并做出相应反应。

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

关键优势:使用应用事件替代直接方法调用,使我们能保持更松散的耦合,并反转两个模块间的依赖关系。简单说,"订单"模块不需要知道"奖励"模块的具体实现。

3. 测试事件监听器

我们可以通过在测试中直接发布应用事件来测试使用@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 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. 测试事件发布器

测试发布应用事件的组件时,可以在测试包中创建自定义事件监听器。这个监听器同样使用@EventListener注解,但会将所有入站事件收集到列表中:

@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收到了一个包含正确属性的事件:

@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

这种架构风格通过应用事件促进模块间的灵活交互。因此,Spring Modulith的组件之一专门支持测试涉及应用事件的流程。

添加Maven依赖spring-modulith-starter-test

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

这让我们能用声明式方式编写测试,使用Scenario API。首先创建测试类并标注@ApplicationModuleTest,然后就能在测试方法中注入Scenario对象:

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

核心功能:这个DSL提供了便捷的测试常见用例的方式,支持:

  • 执行方法调用
  • 发布应用事件
  • 验证状态变更
  • 捕获并验证出站事件

额外实用工具:

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

5.2 使用Scenario API测试事件监听器

之前测试@EventListener组件时需要注入ApplicationEventPublisher并手动发布事件。现在用Scenario API的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测试事件发布器

Scenario API也能模拟方法调用,拦截并验证出站的应用事件。用DSL编写测试验证"订单"模块的行为:

@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应用事件代码的多种方式:

  1. 手动测试:使用ApplicationEventPublisher发布事件,或创建自定义TestEventListener捕获事件
  2. Spring Modulith方案:利用Scenario API以声明式方式编写测试

流式DSL的优势:

  • ✅ 简化事件发布和捕获
  • ✅ 支持方法调用模拟
  • ✅ 内置异步状态变更等待机制

完整源码可在GitHub获取。


原始标题:How to Test Spring Application Events | Baeldung