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应用事件代码的多种方式:
- 手动测试:使用
ApplicationEventPublisher
发布事件,或创建自定义TestEventListener
捕获事件 - Spring Modulith方案:利用Scenario API以声明式方式编写测试
流式DSL的优势:
- ✅ 简化事件发布和捕获
- ✅ 支持方法调用模拟
- ✅ 内置异步状态变更等待机制
完整源码可在GitHub获取。