31.Axon框架-测试

25 阅读5分钟

Axon框架-测试

1.聚合测试

介绍

CQRS和事件溯源的优势之一在于,它允许完全用Event和Command来表达。Event和Command都是功能组件,对于领域专家或业务负责人来说,它们的含义清晰明确。这不仅意味着用Event和Command表达的测试具有清晰的功能意义,也意味着它们几乎不依赖于任何具体的实现方式

在Axon4中,测试Aggregate是基于行为驱动的,采用清晰的Given-When-Then (假设-当-那么)模式。这种基于Command和Event的测试方式,让测试用例能够直接反映业务需求,而无需依赖数据库等底层基础设施

本章介绍的测试方法适用于任何测试框架,例如JUnit和TestNG

AAA模式与GWT模式对比

步骤

  • 给定一段历史事件
  • 向聚合发送一个命令
  • 断言聚合产生了什么事件、返回了什么结果、抛了什么异常、是否被删除

依赖

    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-test</artifactId>
        <scope>test</scope>
        <version>4.6.0</version>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest</artifactId>
        <scope>test</scope>
    </dependency>

注意:hamcrest是Axon4.6某些断言API运行时依赖,缺少时会出现org.hamcrest.Description找不到的错误

核心组件AggregateTestFixture

介绍

聚合测试的核心是AggregateTestFixture。它的作用是为单个聚合配置并建立测试环境(包括配置独立的CommandBus、EventBus和EventStore),从而让我们能够在隔离的环境中测试命令处理器@CommandHandler

它可以让你以GWT来表达你的单元测试

通常,我们需要在每次测试前(如@BeforeEach或@Before)初始化一个新的Fixture,以保证各测试用例之间相互独立

JUni4初始化
public class OrderAggregateTest {

    private FixtureConfiguration<OrderAggregate> fixture;

    @BeforeEach
    public void setUp() {
        // 针对 OrderAggregate 初始化 Fixture
        fixture = new AggregateTestFixture<>(OrderAggregate.class);
    }
}
JUnit5初始化

public class AggregateTest {

@RegisterExtension
static StubAggregateLifecycleExtension testSubject = new StubAggregateLifecycleExtension();

   @Test
   void test() {
       apply(new Event(...));

       assertEquals(1, testSubject.getAppliedEvents().size());
   }  
 
}
工作流程
  • Given(前置状态阶段):
    • givenNoPriorActivity():表示没有任何历史活动,用于测试聚合的创建过程
    • given(Object...):传入一系列历史事件,Fixture会自动重放这些事件来重建(Event Source)聚合的当前状态
    • givenCommands(Object...):传入历史命令,Fixture会通过执行这些命令所产生的事件来重建聚合状态
  • When(执行阶段):触发需要被测试的操作:
    • when(Object command):发送你想要测试的命令。Fixture将只监控此阶段发生的行为和副作用
  • Expect/Then(验证阶段):校验执行命令后的结果
    • expectEvents(Object...):验证命令处理完成后是否准确发布了预期的事件序列
    • expectException(Class):如果命令包含非法业务逻辑,验证是否按预期抛出了异常
    • expectSuccessfulHandlerExecution():验证命令处理器是否成功执行且未抛出异常
fixture.given(历史事件...)
       .when(命令)
       .expectEvents(期望事件...)
       .expectResultMessagePayload(期望返回值);

示例

public class OrderAggregateTest {

    private FixtureConfiguration<OrderAggregate> fixture;
    private final String orderId = UUID.randomUUID().toString();

    @BeforeEach
    public void setUp() {
        fixture = new AggregateTestFixture<>(OrderAggregate.class);
    }

    @Test
    public void testCreateOrder() {
        // 测试命令:创建订单
        fixture.givenNoPriorActivity()
               .when(new CreateOrderCommand(orderId, "Deluxe Chair"))
               .expectSuccessfulHandlerExecution()
               .expectEvents(new OrderCreatedEvent(orderId, "Deluxe Chair"));
    }

    @Test
    public void testShipUnconfirmedOrderThrowsException() {
        // 测试命令:尝试在未确认的情况下发货订单,应该抛出异常
        fixture.given(new OrderCreatedEvent(orderId, "Deluxe Chair")) // 重建历史状态
               .when(new ShipOrderCommand(orderId))
               .expectException(UnconfirmedOrderException.class); // 期望抛出异常
    }
}

自动检测非法的状态修改

在事件溯源架构中,聚合的内部状态应该只在@EventSourcingHandler标注的方法内进行修改。如果开发人员不小心在@CommandHandler中直接修改了聚合的状态字段,将会导致在生产环境重建聚合时状态不一致

AggregateTestFixture默认开启了非法状态更改检测机制。它会在命令执行后检查聚合状态,并将其与纯靠事件重建出的状态进行比对。如果不一致,测试将直接失败

处理聚合内的依赖注入

如果你的@CommandHandler在处理Command时需要调用外部服务(例如调用校验服务),你可以通过Fixture将Mock的服务注入到聚合中

fixture.registerInjectableResource(myMockedService);

此方法会自动将资源注入到聚合内部标注了@Inject或@Autowired的字段/方法中

时间与截止日期测试

如果你的聚合业务涉及时间(例如超时未付款自动取消),Fixture会在创建时冻结时间。这让基于时间的测试变得非常确定和可靠

  • givenCurrentTime(Instant)设定起始时间
  • 在When阶段使用whenTimeElapses(Duration)或whenTimeAdvancesTo(Instant)来模拟时间流逝
  • 在Expect阶段使用 expectScheduledDeadline(...)或expectDeadlinesMet(...)来验证某个Deadline消息是否如期触发

验证内部状态

虽然Fixture提供了expectState(Consumer)方法允许你通过断言检查聚合内部字段的变化,但这在事件溯源聚合中被认为是反模式。聚合的状态对于测试代码应当是完全不透明的(黑盒测试),只需验证向外发出的事件(行为)是否正确。如果你觉得必须要验证状态,那通常意味着遗漏了某些验证事件的测试场景

常见语法示例

// 测试创建聚合
fixture.givenNoPriorActivity()
       .when(command)
       .expectEvents(expectedEvent);

// 有历史事件
fixture.given(
        event1,
        event2
)
       .when(command)
       .expectEvents(expectedEvent);
       
// 断言事件,它会严格校验:事件数量,事件顺序,事件类型,事件payload是否相等
.expectEvents(event1, event2)

// 断言Commnad返回值
.expectResultMessagePayload(expectedResult)

// 断言成功执行
.expectSuccessfulHandlerExecution()

// 断言异常,如果还要进一步精确断言异常内容,可以继续用.expectExceptionMessage("xxx")或Hamcres做更灵活的匹配
.expectException(DomainException.class)

// 断言聚合被删除
.expectMarkedDeleted()

// 断言状态
.expectState(aggregate -> {
    // 断言聚合内部状态
});

2.Saga测试

3.读模型测试

4.集成测试

5.总结