1、概览 {#1概览}
本文将带你了解如何测试 Spring Application Event,以及如何使用 Spring Modulith 的测试库。
2、Application Event {#2application-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 {#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 {#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 的测试支持 {#5spring-modulith-的测试支持}
Spring Modulith 提供了一系列可独立使用的工具库。这些库提供了一系列功能,主要目的是在应用的逻辑模块之间建立清晰的界限。
5.1、Scenario API {#51scenario-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 {#52使用-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 {#53使用-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、总结 {#6总结}
本文介绍了对 Spring Application Event 进行测试的各种方法。首先介绍了使用 ApplicationEventPublisher
手动发布 Application Event 进行测试,最后介绍了 Spring Modulith 的测试支持,并使用 Scenario
API 以声明方式编写了相同的测试。Fluent 风格的 DSL 使我们能够发布和捕获 Application Event、模拟方法调用并等待状态变化。
Ref:https://www.baeldung.com/spring-test-application-events