CQRS最大的好处之一,尤其是event sourcing的优点之一是可以纯粹用事件和命令来进行测试。两者都是功能组件,事件和命令对领域专家或企业所有者具有明确的含义。 这不仅意味着根据事件和命令表达的测试具有明确的功能意义,还意味着它们几乎不依赖任何实现。
本章描述的特性需要axon-test模块,可以通过配置maven依赖关系(使用<artifactId>axon-test </ artifactId>和<scope> test </ scope>)或从完整软件包下载。
本章描述的固件可以用于任何测试框架,比如JUnit和TestNG。
命令组件测试
命令处理组件通常是任何基于CQRS的体系结构中包含最大复杂的性的组件。因为它比其他的部分更复杂,这也意味着这个组件有额外的测试相关要求。虽然更复杂,但命令处理组件的API相当容易。 一个命令进来,会产生事件,在某些情况下,查询可能会作为命令执行的一部分, 此外,命令和事件也可能是API的唯一部分, 这意味着可以根据事件和命令完整地定义测试场景。 通常是这样的形式:
- 提供某些过去发生的事件;
- 执行命令
- 预期这些事件被发布和/或存储
Axon框架提供了一个测试装置,可以让您做到这一点。 AggregateTestFixture允许您配置由必要的命令处理程序和存储库组成的特定基础架构,并根据即时发生的事件和命令来表达您的场景。以下示例显示了通过JUnit 4进行given-when-then测试的用法:
public class MyCommandComponentTest { private FixtureConfiguration<MyAggregate> fixture; @Before public void setUp() { fixture = new AggregateTestFixture<>(MyAggregate.class); } @Test public void testFirstFixture() { fixture.given(new MyEvent(1)).when(new TestCommand()).expectSuccessfulHandlerExecution().expectEvents(new MyEvent(2)); /* 这四行定义了实际情景及其预期结果。 第一行定义了过去发生的事件。 这些事件定义了被测试聚合的状态。 实际上,这些是事件是在加载聚合时从事件存储中返回的。 第二行定义了我们希望对我们的系统执行的命令。 最后,我们有两个定义预期行为的方法。 在这个例子中,我们使用建议的void返回类型。 最后一个方法定义我们期望命令执行的结果是单个事件。 */ } }
给定时间测试装置定义了三个阶段:配置,执行和验证。每个阶段都由不同的接口表示:FixtureConfiguration,TestExecutor和ResultValidator。 Fixtures类中的静态方法newGivenWhenThenFixture()提供了对第一个的引用,它挨个儿又可以提供验证器的引用,等等。为了让这些阶段之间的迁移变得更方便,最好使用这些方法提供的流畅接口,如上例所示。
在配置阶段(即在提供第一个“给定”之前),您提供执行测试所需的构建块,事件总线、命令总线和事件存储器的特定版本作为测试装置的一部分提供,测试装置内的一些方法将获取对它们的引用,任何未直接在聚合上注册的命令处理程序都需要使用registerAnnotatedCommandHandler方法进行显式配置,除了带注释的命令处理程序外,您还可以配置各种组件和设置,以定义应该如何设置测试周围的基础架构。
一旦装置被配置,您可以定义“给定”事件。 测试装置将把这些事件包装为DomainEventMessage。 如果“给定”事件实现了Message,则该消息的有效载荷和元数据将包含在DomainEventMessage中,否则给定事件将用作有效载荷。 DomainEventMessage的顺序号是连续的,从0开始。
或者,您也可以将命令提供为“给定”场景。 在这种情况下,这些命令生成的事件,将在测试并执行实际命令时,作为聚合的事件源。通过使用 "givenCommands (...) "方法来提供命令对象,允许您在执行阶段针对命令处理组件提供要执行的命令。被调用的处理程序 (聚合或外部处理程序) 的行为被监视, 并与验证阶段中注册的期望进行比较。
注意:在执行测试期间,Axon会尝试检测受测试的聚合中的任何非法状态变化。它通过将命令执行后的Aggregate的状态,与源自所有“给定”和存储事件的Aggregate的状态进行比较。 如果该状态不相同,这意味着状态更改发生在Aggregate的事件处理方法之外。 比较中会忽略静态和瞬态字段,因为它们通常包含对资源的引用。您可以使用setReportIllegalStateChange方法在装置的配置中切换检测。
最后一个阶段是验证阶段,允许您检查命令处理组件的活动。 这完全是根据返回值和事件完成的。
测试装置允许您验证命令处理程序的返回值。 您可以明确定义预期的返回值,或者只需要该方法成功返回。 您也可以表达您希望CommandHandler抛出的任何异常。
另一个组件是发布事件的验证。有两种匹配预期事件的方法。第一种是传入需要与实际事件进行字面比较的事件实例。 预期事件的所有属性(使用equals())与实际事件中的对应项进行比较。如果其中一个属性不相同,则测试失败并生成额外的错误报告。
表达期望的另一种方式是使用Matchers(由Hamcrest库提供)。Matcher是一个含有两种方法的接口:matches(Object)和describeTo(Description)。 第一个返回一个布尔值来指示匹配器是否匹配。 第二个可以让你用文字表达你的期望。 例如,“GreaterThanTwoMatcher”匹配器可以将“anyevent with value greater than two”的文字附加到描述中,描述中可以创建关于为什么测试用例失败的表述性的错误消息。
为事件列表创建匹配器可能是乏味且容易出错的工作。为了简化事情,Axon提供了一组匹配器,允许您提供一组事件特定的匹配器,并告诉Axon他们应该如何匹配列表。以下是可用事件列表匹配器及其用途的概述。
列出所有:Matchers.listWithAllOf(事件匹配器...)
如果所有提供的事件匹配器与实际事件列表中的至少一个事件匹配,则匹配器将成功。 多个匹配器是否与同一个事件匹配并不重要,也不管该列表中的事件是否与任何匹配器匹配。
列出任何一个:Matchers.listWithAnyOf(事件匹配器...)
如果提供的一个或多个事件匹配器与实际事件列表中的一个或多个事件匹配,则此匹配器将成功。 一些匹配者可能根本不匹配,而另一匹配匹配多个其他匹配。
顺序事件:Matchers.sequenceOf(事件匹配器...)
使用此匹配器验证实际事件是否与提供的事件匹配器以相同的顺序匹配。 如果每个匹配器匹配了上一个匹配器所匹配的事件之后发生的事件,它将成功。 这意味着可能会无法匹配到事件。也就是说,如果在评估完事件之后,还有更多匹配器可用,则它们全部接收到“null”。 由这些匹配器将决定是否接受“null”。
事件精确序列:Matchers.exactSequenceOf(事件匹配器...)
与“顺序事件”匹配器不同,差别在于无法匹配的事件是不允许的,这意味着每个匹配器都必须直接匹配到上一个的事件所匹配的事件之后发生的事件,这意味着不允许无法匹配到事件。
为了方便起见,提供了一些通常需要的事件匹配器。它们与单个Event实例匹配:
事件比较:Matchers.equalTo(实例...)
验证给定对象在语义上等于给定事件。 这个匹配器将使用一个空指针安全的方法比较实际和期望对象字段中的所有值。 这意味着可以比较事件,即使它们没有实现equals方法。 存储在给定参数的字段中的对象使用equals进行比较,要求它们正确实现equals方法。
没有更多事件:Matchers.andNoMore()或Matchers.nothing()
只匹配空值。 此匹配器可以作为添加到“事件精确序列”匹配器中的最后一个匹配器,以确保不会有更多事件。
由于匹配器被传递了一个事件消息列表,所以你有时只想验证消息的有效载荷。 有匹配者可以帮助你:
Payload匹配器:Matchers.messageWithPayload(payloadmatcher)
验证消息的Payload与给定的匹配器匹配。
payload列表匹配器:Matchers.payloadsMatching(列表匹配器)
验证消息的payload列表与给定的匹配器匹配。 给定的匹配器必须与含有每个消息的payload列表匹配。 payload匹配器通常用作外部匹配器来防止payload匹配器的重复。
以下是显示这些匹配器使用情况的代码示例。 在这个例子中,我们预计将发布两个事件。 第一个事件必须是“ThirdEvent”,而第二个“aFourthEventWithSomeSpecialThings”。 可能没有第三个事件,因为这将不符合“和NoMore”匹配器。
fixture.given(new FirstEvent(), new SecondEvent()) .when(new DoSomethingCommand("aggregateId")) .expectEventsMatching(exactSequenceOf( //we can match against the payload only: messageWithPayload(equalTo(new ThirdEvent())), //this will match against a Message aFourthEventWithSomeSpecialThings(), //this will ensure that there are no more events andNoMore() )); // or if we prefer to match on payloads only: .expectEventsMatching(payloadsMatching( exactSequenceOf( //we only have payloads, so we can equalTo directly equalTo(new ThirdEvent()), //now, this matcher matches against the payload too aFourthEventWithSomeSpecialThings(), //this still requires that there is no more events andNoMore() ) ));
测试注释的Sagas
与命令处理组件类似,Sagas有明确定义的接口:它们只响应事件。 另一方面,Saga通常会有时间概念,并可能与其他组件进行交互是其事件处理过程的一部分。 Axon Framework的测试支持模块包含可帮助您编写Saga测试的固件。每个测试装置包含三个阶段,类似于前一节中所述的命令处理组件装置。
每个测试装置包含三个阶段,类似于前一节中所述的命令处理组件装置。
- 给定某些事件(来自某些聚合),
- 当事件到达或时间流逝时,
- 期望某些行为或状态
“给定事件”阶段和“事件到达”阶段接受事件作为其交互的一部分。 在“给定事件”阶段,如果可能,所有副作用(如生成的命令)都将被忽略。 另一方面,在“事件到达”阶段,从Saga产生的事件和命令被记录并且可以被验证。以下代码示例显示了如何使用测试装置z做测试:在30天内未支付发票时发送通知.
FixtureConfiguration<InvoicingSaga> fixture = new SagaTestFixture<>(InvoicingSaga.clas s); fixture.givenAggregate(invoiceId).published(new InvoiceCreatedEvent()) .whenTimeElapses(Duration.ofDays(31)) .expectDispatchedCommandsMatching(Matchers.listWithAllOf(aMarkAsOverdueCommand( ))); // or, to match against the payload of a Command Message only .expectDispatchedCommandsMatching(Matchers.payloadsMatching(Matchers.listWithAl lOf(aMarkAsOverdueCommand())));
Sagas可以使用回调来调度命令,以通知命令处理结果。由于在测试中没有实际的命令处理,因此使用CallbackBehavior对象定义行为。 该对象在fixture上使用setCallbackBehavior()进行注册,并定义在调度命令时是否以及如何调用回调。
不用直接使用CommandBus,也可以使用命令网关,具体如何操作,请参阅下面的内容。
通常,Sagas将与资源进行交互。 这些资源不是Saga的一部分,而是在加载或创建Saga后注入的。 测试装置允许您注册需要注入Saga的资源。 要注册资源,只需使用资源作为参数调用fixture.registerResource(Object)方法即可。 装置将检测到Saga的适当的setter方法或字段(用@Inject注解),并用注入资源。
将模拟对象(例如Mockito或Easymock)注入到Saga中可能非常有用。 它允许您验证saga与您的外部资源正确交互。
命令网关为Saga提供了一种更简单的方式来发送命令。 使用自定义命令网关还可以更容易地创建模拟或存根来定义其在测试中的行为。 但是,当提供模拟或存根时,可能不会调度实际的命令,从而无法验证测试装置中发送的命令。
因此,装置提供了两种方法,允许您注册命令网关以及定义行为的模拟对象:registerCommandGateway(Class)和registerCommandGateway(Class,Object)。 两种方法都会返回代表要使用的网关的给定类的实例,此实例同时也可以被作为注入的资源使用。当registerCommandGateway(Class)被用来注册一个网关时,它将Command发送到由装置管理的CommandBus。 网关的行为主要由在装置上定义的CallbackBehavior定义。如果未提供明确的CallbackBehavior,则不会调用回调,因此无法为网关提供任何返回值。
当使用registerCommandGateway(Class,Object)注册网关时,第二个参数用于定义网关的行为。测试装置会尽可能地加快模拟系统时间流逝。 这意味着在测试执行时,似乎没有时间过去,除非您使用whenTimeElapses()明确声明。 所有事件都会有测试装置创建时刻的时间戳。
在测试期间停止测试装置的时间模拟可以更容易地预测事件计划发布的时间。 如果您的测试用例验证事件计划在30秒内发布,则无论实际调度和测试执行之间的时间如何,它都将保持30秒。
注意:装置使用StubScheduler进行基于时间的活动,例如安排活动和推进时间。装置将设置发送到Saga实例的任何事件的时间戳为该调度程序的时间。 这意味着一旦装置启动后时间是“停滞”的,并且可以使用whenTimeAdvanceTo和whenTimeElapses方法确定性地提前。
如果您需要测试事件的时间安排,您还可以独立于测试装置使用StubEventScheduler。 该EventScheduler实现允许您验证哪些事件计划在哪个时间进行,并为您提供操作时间进度的选项。 您可以使用特定的持续时间提前时间,将时钟移动到特定的日期和时间,或者将时间提前到下一个计划的事件。 所有这些操作都将返回给进度区间内安排的事件。