Spring Modulith 应用模块化测试:事件发布与订阅的精细化验证
大家好,今天我们来深入探讨 Spring Modulith 应用中模块化测试的核心概念:事件发布与订阅的测试策略。Spring Modulith 旨在通过模块化架构提升应用的可维护性和可扩展性,而有效的模块化测试则是确保这一目标的关键。我们将重点关注 ApplicationModuleTest 和 @ModuleTest 注解的使用,以及如何利用它们来验证模块间事件的正确发布和订阅。
Spring Modulith 模块化测试概述
在传统的单体应用中,测试往往是端到端的,难以隔离和验证特定模块的行为。Spring Modulith 通过引入模块的概念,允许我们将应用分解为独立的、职责明确的模块。每个模块可以独立开发、测试和部署,从而降低了复杂性,提高了开发效率。
模块化测试的核心思想是:针对每个模块进行独立的单元测试和集成测试,同时验证模块之间的协作关系。Spring Modulith 提供了 ApplicationModuleTest 和 @ModuleTest 注解,简化了模块化测试的配置和执行。
ApplicationModuleTest: 用于启动一个包含特定模块的 Spring 上下文,模拟应用的运行环境。它会自动配置 Spring Modulith 的基础设施,例如事件发布机制。@ModuleTest: 用于声明需要包含在测试上下文中的模块。它可以用于类级别,表示整个测试类都针对该模块;也可以用于方法级别,表示特定的测试方法针对该模块。
事件发布与订阅的测试挑战
在模块化的应用中,模块间的通信通常通过事件来实现。一个模块发布事件,另一个模块监听并处理该事件。因此,验证事件的正确发布和订阅是模块化测试的重要组成部分。
常见的测试挑战包括:
- 事件是否被正确发布?: 发布模块是否在正确的时机发布了事件?事件的内容是否正确?
- 事件是否被正确订阅?: 订阅模块是否监听到了事件?处理事件的逻辑是否正确?
- 事件处理的事务性: 事件处理是否在同一个事务中进行?如果事件处理失败,事务是否回滚?
- 异步事件处理: 如何测试异步事件处理的逻辑?
使用 ApplicationModuleTest 和 @ModuleTest 进行事件发布测试
首先,我们创建一个简单的 Spring Modulith 应用,包含两个模块:OrderModule 和 InventoryModule。OrderModule 负责处理订单,InventoryModule 负责管理库存。当一个订单被创建时,OrderModule 会发布一个 OrderCreatedEvent,InventoryModule 监听该事件并减少库存。
1. 定义事件:
package com.example.order;
import java.util.UUID;
public record OrderCreatedEvent(UUID orderId, int quantity) {}
2. 定义 OrderModule:
package com.example.order;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public UUID createOrder(int quantity) {
UUID orderId = UUID.randomUUID();
publisher.publishEvent(new OrderCreatedEvent(orderId, quantity));
return orderId;
}
}
3. 定义 InventoryModule:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class InventoryService {
private int stock = 100;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
stock -= event.quantity();
}
public int getStock() {
return stock;
}
}
4. 创建测试类:
package com.example.order;
import com.example.inventory.InventoryService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.test.context.ContextConfiguration;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ApplicationModuleTest
class OrderModuleTests {
@Autowired
OrderService orderService;
@Autowired
InventoryService inventoryService;
@Autowired
ApplicationModules modules;
@Test
void shouldPublishOrderCreatedEvent() {
ApplicationModule orderModule = modules.getModule("order").orElseThrow();
ApplicationModule inventoryModule = modules.getModule("inventory").orElseThrow();
assertThat(orderModule.canSee(inventoryModule)).isTrue(); // 检查模块可见性
int initialStock = inventoryService.getStock();
UUID orderId = orderService.createOrder(10);
assertThat(inventoryService.getStock()).isEqualTo(initialStock - 10);
}
}
在这个测试中:
@SpringBootTest和@ApplicationModuleTest注解告诉 Spring Boot 创建一个包含所有模块的应用程序上下文。- 我们注入了
OrderService和InventoryService,以便可以调用它们的方法。 ApplicationModules被注入,用于获取对各个模块的引用,并验证模块之间的可见性。shouldPublishOrderCreatedEvent测试方法调用orderService.createOrder()方法,然后验证InventoryService的库存是否减少。assertThat(orderModule.canSee(inventoryModule)).isTrue()验证了模块间的可见性,确保OrderModule可以访问InventoryModule。
使用 @ModuleTest 精细化测试
@ModuleTest 允许我们更精细地控制测试上下文。我们可以指定哪些模块应该包含在测试上下文中,以及哪些模块应该被排除在外。
1. 创建针对 OrderModule 的测试类:
package com.example.order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.modulith.test.ModuleTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import java.util.UUID;
import static org.mockito.Mockito.*;
@SpringBootTest
@ModuleTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class
})
class OrderModuleOnlyTests {
@Autowired
OrderService orderService;
@Autowired
ApplicationEventPublisher publisher;
@Test
void shouldPublishOrderCreatedEvent() {
// 使用 Mockito 验证事件是否被发布
OrderService spy = spy(orderService);
doNothing().when(spy).createOrder(anyInt());
UUID orderId = spy.createOrder(5);
verify(publisher, times(1)).publishEvent(any(OrderCreatedEvent.class));
}
}
在这个测试中:
@ModuleTest注解告诉 Spring Boot 只包含OrderModule及其依赖项。InventoryModule将不会被包含在测试上下文中。- 我们注入了
OrderService和ApplicationEventPublisher。 shouldPublishOrderCreatedEvent测试方法使用 Mockito 验证OrderService是否发布了OrderCreatedEvent。由于InventoryModule不在测试上下文中,因此我们无法直接验证库存是否减少。- 我们使用
spy来监控OrderService的行为,并使用verify来验证publishEvent方法是否被调用。
2. 创建针对 InventoryModule 的测试类:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.modulith.test.ModuleTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ModuleTest
class InventoryModuleTests {
@Autowired
InventoryService inventoryService;
@Test
void shouldReduceStockWhenOrderCreatedEventReceived() {
int initialStock = inventoryService.getStock();
inventoryService.onOrderCreated(new OrderCreatedEvent(java.util.UUID.randomUUID(), 5));
assertThat(inventoryService.getStock()).isEqualTo(initialStock - 5);
}
}
在这个测试中:
@ModuleTest注解告诉 Spring Boot 只包含InventoryModule及其依赖项。- 我们注入了
InventoryService。 shouldReduceStockWhenOrderCreatedEventReceived测试方法直接调用inventoryService.onOrderCreated()方法,然后验证库存是否减少。
事件处理的事务性测试
在某些情况下,我们需要确保事件处理是在同一个事务中进行的。这意味着如果事件处理失败,事务应该回滚,以保持数据的一致性。
为了测试事件处理的事务性,我们可以使用 @TransactionalEventListener 注解,并结合 Spring 的事务管理机制。
1. 修改 InventoryService:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class InventoryService {
private int stock = 100;
@TransactionalEventListener
@Transactional
public void onOrderCreated(OrderCreatedEvent event) {
stock -= event.quantity();
if (stock < 0) {
throw new IllegalStateException("库存不足");
}
}
public int getStock() {
return stock;
}
}
在这个修改后的 InventoryService 中:
- 我们使用了
@TransactionalEventListener注解来监听OrderCreatedEvent。 - 我们使用了
@Transactional注解来确保事件处理是在一个事务中进行的。 - 如果库存不足,我们会抛出一个
IllegalStateException异常,这将导致事务回滚。
2. 创建事务性测试类:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@ApplicationModuleTest
@Transactional
class TransactionalInventoryModuleTests {
@Autowired
InventoryService inventoryService;
@Autowired
ApplicationEventPublisher publisher;
@Test
void shouldRollbackTransactionWhenInventoryInsufficient() {
int initialStock = inventoryService.getStock();
assertThrows(IllegalStateException.class, () -> {
publisher.publishEvent(new OrderCreatedEvent(java.util.UUID.randomUUID(), 200));
});
assertThat(inventoryService.getStock()).isEqualTo(initialStock);
}
}
在这个测试中:
- 我们使用了
@Transactional注解来确保整个测试方法是在一个事务中进行的。 shouldRollbackTransactionWhenInventoryInsufficient测试方法发布一个OrderCreatedEvent,导致InventoryService抛出一个IllegalStateException异常。- 我们使用
assertThrows来验证异常是否被抛出。 - 我们验证
InventoryService的库存是否保持不变,以确保事务回滚。
异步事件处理的测试
如果事件处理是异步的,我们需要使用不同的策略来测试。常见的策略包括:
- 使用
TestTransaction.flagForCommit()和TestTransaction.end(): 允许我们在测试中控制事务的提交。 - 使用
@Async和CompletableFuture: 异步处理事件,并使用CompletableFuture来等待事件处理完成。 - 使用消息队列: 使用消息队列来异步处理事件,并使用消息队列的 API 来验证事件是否被正确发送和接收。
由于篇幅限制,我们这里只提供一个简单的使用 @Async 和 CompletableFuture 的示例。
1. 修改 InventoryService:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Component
public class InventoryService {
private int stock = 100;
@EventListener
@Async
public CompletableFuture<Void> onOrderCreated(OrderCreatedEvent event) {
return CompletableFuture.runAsync(() -> {
stock -= event.quantity();
});
}
public int getStock() {
return stock;
}
}
在这个修改后的 InventoryService 中:
- 我们使用了
@Async注解来异步处理OrderCreatedEvent。 onOrderCreated方法返回一个CompletableFuture<Void>,允许我们等待事件处理完成。
2. 创建异步测试类:
package com.example.inventory;
import com.example.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.modulith.test.ApplicationModuleTest;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ApplicationModuleTest
class AsyncInventoryModuleTests {
@Autowired
InventoryService inventoryService;
@Autowired
ApplicationEventPublisher publisher;
@Test
void shouldReduceStockAsynchronously() throws ExecutionException, InterruptedException {
int initialStock = inventoryService.getStock();
publisher.publishEvent(new OrderCreatedEvent(java.util.UUID.randomUUID(), 5));
// 等待异步事件处理完成
Thread.sleep(1000); // 简单等待,实际应用中应使用更可靠的同步机制
assertThat(inventoryService.getStock()).isEqualTo(initialStock - 5);
}
}
在这个测试中:
- 我们使用了
Thread.sleep()来等待异步事件处理完成。注意:这只是一个简单的示例,在实际应用中,我们应该使用更可靠的同步机制,例如CompletableFuture.get()或CountDownLatch。 - 我们验证
InventoryService的库存是否减少。
总结和最佳实践
- 明确模块边界: 在设计模块时,要明确模块的职责和边界,避免模块间的耦合。
- 使用事件驱动架构: 使用事件来解耦模块,提高应用的可扩展性。
- 编写独立的模块测试: 针对每个模块编写独立的单元测试和集成测试,验证模块的行为。
- 验证事件的发布和订阅: 确保事件被正确发布和订阅,并且事件处理的逻辑正确。
- 考虑事件处理的事务性: 如果需要确保事件处理的事务性,可以使用
@TransactionalEventListener注解。 - 测试异步事件处理: 如果事件处理是异步的,可以使用
CompletableFuture或消息队列来测试。
模块化测试是保障系统稳定性和可维护性的关键环节
通过 ApplicationModuleTest 和 @ModuleTest 的灵活运用,我们可以构建出健壮且易于维护的 Spring Modulith 应用,确保每个模块的功能正确,并且模块间的协作无缝衔接。理解并应用这些测试策略,对于构建高质量的模块化应用至关重要。