Spring Modulith应用模块化测试事件发布订阅:ApplicationModuleTest与@ModuleTest

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 应用,包含两个模块:OrderModuleInventoryModuleOrderModule 负责处理订单,InventoryModule 负责管理库存。当一个订单被创建时,OrderModule 会发布一个 OrderCreatedEventInventoryModule 监听该事件并减少库存。

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 创建一个包含所有模块的应用程序上下文。
  • 我们注入了 OrderServiceInventoryService,以便可以调用它们的方法。
  • 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 将不会被包含在测试上下文中。
  • 我们注入了 OrderServiceApplicationEventPublisher
  • 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(): 允许我们在测试中控制事务的提交。
  • 使用 @AsyncCompletableFuture: 异步处理事件,并使用 CompletableFuture 来等待事件处理完成。
  • 使用消息队列: 使用消息队列来异步处理事件,并使用消息队列的 API 来验证事件是否被正确发送和接收。

由于篇幅限制,我们这里只提供一个简单的使用 @AsyncCompletableFuture 的示例。

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 应用,确保每个模块的功能正确,并且模块间的协作无缝衔接。理解并应用这些测试策略,对于构建高质量的模块化应用至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注