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.总结