Axon(九)

九、测试

9.1命令/事件测试

   CQRS的一个好处,尤其是事件源的好处之一,就是可以纯粹用事件和命令来表示测试。作为功能组件,事件和命令对领域专家或业务所有者都有明确的意义。这不仅意味着用事件和命令表示的测试具有明确的功能含义,还意味着它们几乎不依赖于任何实现选择。

本章中描述的特性需要axon测试模块,可以通过配置maven依赖项(使用<artifactId>axon test</artifactId>和<scope>test</scope>)或从完整的软件包下载中获得。

本章中描述的fixture可用于任何测试框架,如JUnit和TestNG。

9.1.1命令模型测试

命令处理组件通常是所有基于CQRS的体系结构中所包含最复杂的组件。由于比其他组件更复杂,这也意味着该组件有额外的测试相关需求。

虽然命令处理组件的API比较复杂,但它相当容易实现。它有命令进来,事件出去。在某些情况下,可能会有一个查询作为命令执行的一部分。除此之外,命令和事件是API的唯一部分。这意味着可以根据事件和命令完全定义测试场景。通常,这些场景包括:

  1. 处理过去所给定的某些事件时,
  2. 执行此命令时,
  3. 期望这些事件被发布和/或存储时

Axon框架提供了一个测试夹具(测试框架),可以让您完全做到这一点。AggregateTestFixture允许您配置由必要的命令处理程序和存储库组成的特定基础结构,并用“给定时间”事件和命令来表示您的场景。

测试夹具的焦点

由于这里的测试单元是聚合,AggregateTestFixture只测试一个聚合。因此,when(或given)子句中的所有命令都是针对testfixture下的聚合的。此外,所有给定的和预期的事件都将从测试夹具的聚合中触发。

下面的示例显示了在GiftCard聚合(如前所定义)上使用junit4的“给定时间”测试夹具:

import org.axonframework.test.aggregate.AggregateTestFixture;

import org.axonframework.test.aggregate.FixtureConfiguration;

 

public class GiftCardTest {

 

    private FixtureConfiguration<GiftCard> fixture;

 

    @Before

    public void setUp() {

        fixture = new AggregateTestFixture<>(GiftCard.class);

    }

 

    @Test

    public void testRedeemCardCommand() {

        fixture.given(new CardIssuedEvent("cardId", 100))

               .when(new RedeemCardCommand("cardId", "transactionId", 20))

               .expectSuccessfulHandlerExecution()

               .expectEvents(new CardRedeemedEvent("cardId", "transactionId", 20));

        /*

        These four lines define the actual scenario and its expected result.

        The first line defines the events that happened in the past.

        These events define the state of the aggregate under test.

        In practical terms, these are the events that the event store returns

         when an aggregate is loaded.

 

        The second line defines the command that we wish to execute against our system.

 

        Finally, we have two more methods that define expected behavior.

        In the example, we use the recommended void return type.

        The last method defines that we expect a single event as result

         of the command execution.

        */

    }

}

“给定时间”测试夹具定义了三个阶段:配置、执行和验证。每个阶段都由不同的接口表示:FixtureConfiguration、TestExecutor和ResultValidator。

流畅接口

为了更好地利用这些阶段之间的迁移,最好使用这些方法提供的fluent接口,如上面的示例所示。

9.1.1.1测试设置

在配置阶段(即在提供第一个“给定”之前),您提供执行测试所需的构建块。默认情况下,事件总线、命令总线和事件存储的专门版本作为fixture的一部分提供。有一些访问器方法可以获取对它们的引用。任何未直接在聚合上注册的命令处理程序都需要使用registerNotatedCommandHandler方法显式配置。除了带注释的命令处理程序外,还可以注册各种各样的组件和设置,这些组件和设置定义如何设置聚合测试的基础结构,这些组件和设置包括以下内容:

  1. registerRepository:注册自定义聚合存储库。
  2. registerRepositoryProvider:注册用于生成新聚合的RepositoryProvider。
  3. registerAggregateFactory:注册自定义AggregateFactory。
  4. registerNotatedCommandHandler:注册带注释的命令处理程序对象。
  5. registerCommandHandlerr:注册CommandMessage的MessageHandler。
  6. registerInjectableResource:注册可注入消息处理成员的资源。
  7. registerParameterResolverFactory:将ParameterResolverFactory注册到测试夹具。此方法用于用自定义ParameterResolver补充默认ParameterResolver。
  8. registerCommandDispatchInterceptor:注册命令MessageDispatchInterceptor。
  9. 注册CommandHandlerInterceptor:注册命令MessageHandlerInterceptor。
  10. RegisterReadlineDispatchInterceptor(RegisterReadlineDispatchInterceptor):注册DeadlineMessage的MessageDispatchInterceptor。
  1. registerDeadlineHandlerInterceptor(注册域名:HandlerInterceptor):注册DeadlineMessage 的MessageHandlerInterceptor。
  2. registerFieldFilter:注册在“then”阶段比较对象时使用的字段筛选器。
  3. registerIgnoredField:注册一个在执行状态相等时应忽略给定类的字段。
  4. registerHandlerDefinition:将自定义HandlerDefinition注册到测试设备。
  5. registerHandlerEnhancerDefinition:将自定义HandlerEnhancerDefinition注册到测试设备。此方法用于使用自定义HandlerEnhancerDefinition来补充默认的HandlerEnhancerDefinition。
  6. registerCommandTargetResolver:将CommandTargetResolver注册到测试设备。

一旦配置了fixture,就可以定义“给定”事件。测试夹具将这些事件包装为DomainEventMessages。如果“给定”事件实现消息,则该消息的有效负载和元数据将包含在DomainEventMessage中,否则将使用给定事件作为有效负载。DomainEventMessage的序列号是连续的,从0开始。如果不需要先前的活动,则可以将givenOpriorActivity()用作起点。

或者,您也可以在“给定”场景中提供命令。在这种情况下,当执行测试中的实际命令时,这些命令生成的事件将用于事件源聚合。使用“givenCommands(…)”方法提供命令对象。

“给定”阶段的最后一个选项是直接提供聚合的状态。在事件回溯的情况下不建议这样做,并且仅在无法基于命令或事件重建聚合的情况下,或者在使用状态存储的聚合时才建议这样做。使用固定装置.givenState(()->new GiftCard())定义初始状态。

9.1.1.2测试执行阶段

执行阶段允许您进入验证阶段的两个入口点。首先,您可以提供一个针对命令处理组件执行的命令。与给定事件类似,如果提供的命令属于CommandMessage类型,则将按原样进行调度。被调用的处理程序(在聚合上或作为外部处理程序)的行为将被监视,并与在验证阶段注册的期望值进行比较。

第二,有可能随着时间的推移(持续时间)和时间(即时)处理的时间间隔而变化。这些支持测试死线消息的发布,如本章进一步定义的那样。

注意,只监视在测试执行阶段发生的活动。验证阶段不考虑“给定”阶段产生的任何事件或副作用。

非法状态变化检测

在测试执行期间,Axon尝试检测被测聚合中的任何非法状态更改。它通过将命令执行后的聚合状态与来自所有“给定”和存储事件的聚合状态进行比较来实现这一点。如果该状态不相同,则表示状态更改发生在聚合的事件处理程序方法之外。静态和临时字段在比较中被忽略,因为它们通常包含对资源的引用。

您可以使用setReportIllegalStateChange()方法在fixture的配置中切换检测。

9.1.1.3验证阶段

最后一个阶段是验证阶段,它允许您检查命令处理组件的活动。这通常是纯粹根据返回值和事件来完成的。

9.1.1.3.1验证命令结果

测试夹具允许您验证命令处理程序的返回值。您可以显式定义预期的传回值,或只是要求方法顺利传回。您还可以表示您期望CommandHandler抛出的任何异常。

以下方法可用于验证命令结果:

  1. fixture.expectsuccessfullhandlerExecution():验证处理程序是否返回未标记为异常响应的常规响应。不评估确切的响应。
  2. fixture.expectResultMessagePayload(Object):验证处理程序是否返回了一个成功的响应,有效负载等于给定的负载。
  3. fixture.expectResultMessagePayloadMatching(匹配器):验证处理程序是否返回了成功的响应,有效负载与给定的匹配器匹配
  4. fixture.expectResultMessage(CommandResultMessage):验证接收到的是否具有与给定消息相同的有效负载和元数据。
  5. fixture.expectResultMessageMatching(Matcher):验证CommandResultMessage是否与给定的匹配器匹配。
  6. fixture.expectException(Matcher):验证命令处理结果是否为异常结果,以及异常是否与给定的匹配器匹配。
  7. fixture.expectException(Class):验证命令处理结果是否为具有给定异常类型的异常结果。
  8. fixture.expectExceptionMessage(String):验证命令处理结果是否为异常结果,以及异常消息是否等于给定消息。
  9. fixture.expectExceptionMessage(Matcher):验证命令处理结果是否为异常结果,以及异常消息是否与给定的匹配器匹配。

9.1.1.3.2验证已发布的事件

另一个组件是已发布事件的验证。有两种方法可以匹配预期事件。

第一种方法是传入需要与实际事件进行实际比较的事件实例。预期事件的所有属性都将与实际事件中对应的属性进行比较(使用equals())。如果其中一个属性不相等,则测试将失败,并生成大量错误报告。

实现的另一种方式是使用“Matchers”(由Hamcrest库提供)。Matcher是一个接口,它规定了两个方法:matches(Object)和describeTo(Description)。第一个返回一个布尔值来指示匹配器是否匹配。第二种可以让你表达你的期望。例如,“GreaterThanTwoMatcher”可以将“值大于2的任何事件”附加到描述中。描述允许创建关于测试用例失败原因的表达性错误消息。

为事件列表创建匹配器是一项乏味且容易出错的工作。为了简化操作,Axon提供了一组匹配器,允许您提供一组特定于事件的匹配器,并告诉Axon它们应该如何与列表匹配。这些匹配器通过抽象matchers实用程序类静态可用。

以下是可用事件列表匹配器及其用途的概述:

  1. 列出所有:Matchers.listWithAllOf(事件匹配器…)

如果所有提供的事件匹配器与实际事件列表中的至少一个事件匹配,则此匹配器将成功。多个匹配器是否匹配同一事件并不重要,

如果列表中的某个事件与任何匹配者都不匹配。

  1. 列出下列任何一项:Matchers.listwithany(事件匹配器…)

如果提供的一个或多个事件匹配器与一个或多个匹配,则此匹配器将成功

实际列表中的事件可能出现以下情况:一些匹配器可能根本不匹配,而另一个匹配多个其他匹配。

  1. 事件顺序:Matchers.sequenceOf(事件匹配器…)使用此匹配器验证实际事件与提供的事件匹配程序的顺序是否相同。如果每个匹配器匹配前一个匹配器所匹配的事件之后的事件,则它将成功。这意味着可能会出现不匹配事件的“缺口”。

如果在评估事件之后,有更多的匹配器可用,那么它们都将与“null”匹配。这是由赛事配对者决定他们是否接受。

  1. 事件的确切顺序:Matchers.exactSequenceOf(事件匹配器…)

“事件序列”匹配器的变体,其中不允许出现不匹配事件的间隔。这意味着每个匹配器必须与前一个匹配器匹配的事件直接匹配。

为了方便起见,Axon提供了一些常用的事件匹配器。它们与单个事件实例匹配:

  1. 同等事件:Matchers.equalTo(instance...)验证给定对象在语义上是否等于给定事件。此匹配器将使用空安全等于方法比较实际对象和预期对象字段中的所有值。这意味着可以比较事件,即使它们没有实现equals方法。也可以使用equals比较存储在给定参数字段中的对象,并要求他们正确地执行一个。
  2. 没有剩余事件:Matchers.andNoMore()或Matchers.nothing()只匹配空值。此匹配器可以作为最后一个匹配器添加到事件匹配器的确切序列中,以确保没有不匹配的事件保留。
  3. Predicate match:Matchers.matches(Predicate)或Matchers.predicate(Predicate):创建与指定谓词定义的值匹配的匹配程序。可以在谓词API提供更好的方法来验证结果的情况下使用。

由于向匹配器传递一个事件消息列表,因此有时您只需要验证消息的有效负载。有匹配器帮你解决问题:

  1. 有效载荷匹配(Payload matching):Matchers.messageWithPayload(payload matcher)验证消息的有效负载是否与给定的负载匹配,匹配。
  2. 有效载荷匹配:Matchers.payloadsMatching(list matcher)验证消息的有效负载是否与给定的匹配器匹配。给定的匹配器必须与包含每个消息负载的列表匹配。有效载荷匹配匹配器通常用作外部匹配器,以防止有效载荷匹配器的重复。

下面是一个小代码示例,显示了这些匹配器的用法。在本例中,我们期望发布两个事件。第一件事必须是"ThirdEvent",第二件事是“aFourthEventWithSomeSpecialThings”。可能不会有第三个事件,因为这将失败的“andNoMore”匹配器。

import org.axonframework.test.aggregate.FixtureConfiguration;

 

import static org.axonframework.test.matchers.Matchers.andNoMore;

import static org.axonframework.test.matchers.Matchers.equalTo;

import static org.axonframework.test.matchers.Matchers.exactSequenceOf;

import static org.axonframework.test.matchers.Matchers.messageWithPayload;

import static org.axonframework.test.matchers.Matchers.payloadsMatching;

 

class MyCommandModelTest {

 

    private FixtureConfiguration<MyCommandModel> fixture;

 

    public void testWithMatchers() {

        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()

                   )

               ));

   }

}

9.1.1.3.3验证聚合状态

在某些情况下,可能需要验证试验后聚合的状态。尤其是在给定时场景中,给定也表示初始状态,使用状态存储聚合时也是如此。

fixture提供了一种方法,允许验证聚合的状态,因为它是在执行阶段(例如when状态)之后被验证的。

fixture.givenState(() -> new GiftCard())

       .when(new RedeemCardCommand())

       .expectState(state -> {

           // perform assertions

       });

expectState方法接受聚合类型的消费者。使用测试框架提供的常规断言来断言给定聚合的状态。任何(运行时)异常或错误都将相应地使测试用例失败。

事件回溯聚合状态验证

测试事件回溯聚合的状态被认为是不好的实践。理想情况下,聚合的状态对于测试代码来说是完全不透明的,因为只有行为才应该被验证。通常,验证状态主要是想表明测试套件中缺少某个测试场景。

验证截止日期

验证阶段还提供了一个选项,用于验证给定聚合实例的计划截止日期和已满足的截止日期。您可以通过一个持续时间或一个瞬间,使用显式的equals、Matcher或只是一个deadline类型来验证截止日期消息。

以下方法可用于验证截止日期:

  1. ExpectScheduledReadLine(Duration, Object):

显式地期望在指定的持续时间之后安排一个给定的截止日期。

  1. ExpectScheduledReadLineMatching(Duration, Matcher):

期望在指定的持续时间之后安排与匹配器匹配的截止日期。

  1. expectScheduledDeadlineOfType(Duration, Class):

期望在指定的持续时间之后安排与给定类型匹配的截止日期。

  1. expectScheduledDeadlineWithName(Duration, String):

期望在指定的持续时间之后安排与给定截止日期名称匹配的截止日期。

  1. ExpectScheduledReadLine(Instant, Object):

显式地期望在指定的时刻安排给定的截止日期。

  1. ExpectScheduledReadLineMatching(Instant, Matcher):

期望在指定的时刻安排匹配匹配器的截止日期。

  1. expectScheduledDeadlineOfType(Instant,Class):

期望在指定的时刻安排与给定类型匹配的截止日期。

  1. expectScheduledDeadlineWithName(Instant,String):

期望在指定的瞬间安排与给定截止日期名称匹配的截止日期。

  1. expectNoScheduledDeadlines():

希望没有设置最后期限。

  1. expectNoScheduledDeadlineMatching(Matcher):

希望没有设置能匹配到匹配器的最后期限。

  1. expectNoScheduledDeadlineMatching(Duration,Matcher):

预期在指定的持续时间之后没有与匹配器匹配的截止时间。

  1. expectNoScheduledDeadline(Duration,Object)

显式地期望在指定的持续时间之后不会安排任何给定的截止日期`

  1. expectNoScheduledDeadlineOfType(Duration,class)

预期在指定的持续时间之后不会安排与给定类型匹配的截止日期`

  1. expectNoScheduledDeadlineWithName(Duration,String)

预期在指定的持续时间之后不会安排与给定截止日期名称匹配的截止日期`

  1. expectNoScheduledDeadlineMatching(Instant,Matching):

预计在指定的时刻没有与匹配器匹配的截止日期。

  1. expectNoScheduledDeadline(Instant,Object)

显式地期望在指定的时刻没有指定的截止日期`

  1. expectNoScheduledDeadlineOfType(Instant,class)

预期在指定的时刻没有与给定类型匹配的截止日期`

  1. expectNoScheduledDeadlineWithName(Instant,String)

期望在指定的时刻没有与给定的截止日期名称匹配的截止日期`

  1. expectDeadlinesMet(Object…):

明确地期望一个或几个最后期限被满足。

expectDeadlinesMetMatching(Matcher<List<DeadlineMessage>>):

期望一个或多个匹配的截止日期已经满足。

9.2 saga

与命令处理组件类似,saga有一个明确定义的接口:它们只响应事件。另一方面,saga通常有一个时间的概念,并且可以作为事件处理过程的一部分与其他组件进行交互。Axon框架的测试支持模块包含帮助您编写saga测试的fixture。

每个测试夹具包含三个阶段,与上一节中描述的命令处理组件fixture相似。

  1. 给定某些事件(来自某些集合),
  2. 当一个事件到来或时间流逝时,
  3. 期待某种行为或状态。

“给定”和“何时”阶段都接受事件作为其交互的一部分。在“给定”阶段,所有的副作用,例如生成的命令都会被忽略,如果可能的话。另一方面,在“when”阶段,由saga生成的事件和命令被记录下来,并且可以被验证。

下面的代码示例显示了如何使用fixture测试saga,该saga在发票未在30天内支付时发送通知:

FixtureConfiguration<InvoicingSaga> fixture = new SagaTestFixture<>(InvoicingSaga.class);

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.listWithAllOf(aMarkAsOverdueCommand())));

Sagas可以使用回调来发送命令,以通知命令处理结果。因为在测试中没有实际的命令处理,所以行为是使用CallbackBehavior对象定义的。此对象是使用fixture上的setCallbackBehavior()注册的,并定义在调度命令时是否以及如何调用回调。

您也可以使用命令网关,而不是直接使用命令总线。有关如何指定它们的行为,请参见下文。

通常,saga会与资源互动。这些资源不是saga的一部分,而是在载入或创建saga之后注入的。测试设备允许您注册需要注入到saga中的资源。要注册资源,只需调用fixture.registerResource(对象)以资源作为参数的方法。fixture将在saga上检测适当的setter方法或字段(用@Inject注释),并用可用的资源调用它。

小贴士

在你的saga中注入模拟对象(例如Mockito或Easymock)是非常有用的。它允许您验证saga是否与外部资源正确交互。

命令网关为sagas提供了一种更简单的调度命令的方法。使用自定义命令网关还可以更容易地创建mock或stub来定义其在测试中的行为。但是,当提供mock或stub时,实际的命令可能不会被调度,这使得无法在测试设备中验证发送的命令。

因此,fixture提供了两种方法,允许您注册命令网关和可选的定义其行为的模拟:registerCommandGateway(Class)和registerCommandGateway(Class,Object)。两个方法都返回给定类的实例,该实例表示要使用的网关。此实例也注册为资源,以使其符合资源注入的条件。

当registerCommandGateway(Class)用于注册网关时,它将命令发送到fixture管理的CommandBus。网关的行为主要由fixture上定义的CallbackBehavior定义。如果没有提供显式的CallbackBehavior,则不会调用回调,从而无法为网关提供任何返回值。

当registerCommandGateway(类,对象)用于注册网关时,第二个参数用于定义网关的行为。

测试夹具尽可能消除系统时间的流逝。这意味着在执行测试时不会有时间流逝,除非您使用whenTimeElapses()显式声明。所有事件都将具有创建测试夹具的时间戳。

在测试期间停止时间可以更容易地预测事件计划在什么时间发布。如果测试用例验证某个事件被计划在30秒内发布,那么它将保持30秒,而不管实际调度和测试执行之间花费了多长时间。

注意

fixture对基于时间的活动使用StubScheduler,例如调度事件和推进时间。Fixtures将把发送到Saga实例的任何事件的时间戳设置为这个调度程序的时间。这意味着一旦夹具启动,时间就“停止”,并且可以使用whenTimeAdvanceTo和whenTimeElapses方法确定地提前。

如果需要测试事件调度,也可以独立于测试夹具使用StubEventScheduler。此EventScheduler实现允许您验证为哪个时间安排了哪些事件,并提供了操作时间进度的选项。您可以将时间提前到特定的持续时间,将时钟移到特定的日期和时间,也可以将时间提前到下一个计划的事件。所有这些操作都将返回进度间隔内计划的事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值