Reactor 3 Reference Guide - 选译 (3)Testing

有专属的测试包:

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>

主要包括:

  • StepVerifier - 一步一步地测试序列
  • TestPublisher - 生产数据,测试下游operators的行为
  • 可选Publisher的序列中(比如使用了switchIfEmpty),确保使用某一个

Testing a Scenario with StepVerifier

可以一步一步地定义一个测试场景:下一个事件是什么?希望Flux发射一个特定值?或者接下来的300ms什么都不做?他们都可以通过StepVerifier实现。
比如下面的Flux装饰代码:

public <T> Flux<T> appendBoomError(Flux<T> source) {
  return source.concatWith(Mono.error(new IllegalArgumentException("boom")));
}

为了测试,你想这样验证:首先发射foo,然后发射bar,然后是产生错误消息boom。
使用StepVerifier,测试代码如下:

@Test
public void testAppendBoomError() {
    //需要一个源Flux
  Flux<String> source = Flux.just("foo", "bar"); 

  StepVerifier.create( //增加StepVerifier builder
    appendBoomError(source)) 
    .expectNext("foo") //第一个信号是onNext,值是foo
    .expectNext("bar")
    .expectErrorMessage("boom") //最后一个信号是onError,包含一个boom消息
    .verify(); //触发测试
}

该API是一个builder。通过增加StepVerifier,把序列传给它来做测试。它提供了下列方法:

  • 表达式expectations是关于下一个信号的。如果收到其他信号(或者内容不匹配),测试失败,并带有有意义的AssertionError。比如你可以使用expectNext(T…) ,或者expectNextCount(long)
  • Consume下一个信号。当你想跳过部分序列,或者想为信号内容使用自定义的assertion的时候(比如,想检查onNext事件,并且发射了包含5条数据的列表)。比如你可以使用consumeNextWith(Consumer)
  • 其他操作,比如暂停或者运行任意代码。比如,如果想操纵某上下文或者状态,可能会使用thenAwait(Duration)和then(Runnable)

对于终止事件,相应的expectation方法是expectComplete()和expectError()的变种。最后,你能做些附加配置,然后触发验证,一般是使用verify()或者变种。
如果验证失败,抛出AssertionError。

Better identifying test failures

  • as(String):可用于大多数expect*的后面,描述前面的expectation。如果失败,错误消息包含该描述。终止expectations和verify不能被描述
  • StepVerifierOptions.create().scenarioName(String):使用StepVerifierOptions增加StepVerifier,可以使用scenarioName给整个场景命名。它也会在错误消息中使用

注意,只有在产生自己的AssertionError的StepVerifier方法,会在消息中使用description/name。

Manipulating Time

基于时间的operators,可以使用StepVerifier.withVirtualTime。以避免测试长时间运行。
比如:

StepVerifier.withVirtualTime(() -> Mono.delay(Duration.ofDays(1)))
//... continue expectations here

虚拟时间特性,在Schedulers工厂中加入一个定制Scheduler。由于时间operators默认使用Schedulers.parallel(),现在替换成了VirtualTimeScheduler。重要的是:在虚拟时间调度器激活之后,再实例化该operator。
为提高成功率,该StepVerifier的输入不是简单的Flux,而是Supplier。这样会延迟生成要测试的flux的实例。
一定要确保Supplier<Publisher< T >>的延迟性。该Flux的实例化应该在lambda内。

有两个和时间相关的expectation,不管是否使用虚拟时间都有效:

  • thenAwait(Duration):暂停评估步骤
  • expectNoEvent(Duration):序列可以继续,如果给定时间内有事件,就失败

这两个方法在经典方式下会将线程暂停一些时间,在虚拟时间方式下也会改进虚拟时钟。
expectNoEvent会认为subscription也是一个事件。如果在第一步使用了它,一般会失败,这是因为检测到了subscription信号。此时,可以使用expectSubscription().expectNoEvent(duration)。

为了快速评估Mono.delay的行为,可以这样写代码:

StepVerifier.withVirtualTime(() -> Mono.delay(Duration.ofDays(1)))
    .expectSubscription() 
    .expectNoEvent(Duration.ofDays(1)) //1天内什么都没发生
    .expectNext(0L) //然后发射0
    .verifyComplete(); //完成

Performing Post-execution Assertions with StepVerifier

在最后一个expectation之后,如果想使用assertion API ,而不是触发 verify()。可以使用verifyThenAssertThat()。
它返回StepVerifier.Assertions 对象。这样,在场景胜利结束后,可以断言一些状态元素。

Testing the Context

对于上下文的传播,有一些expectations:

  • expectAccessibleContext:返回一个ContextExpectations,用来设置expectations 。确保调用then()以返回sequence expectations的设置
  • expectNoAccessibleContext:希望测试operators链上,不传播上下文

可以使用StepVerifierOptions,给StepVerifier关联一个测试初始上下文。代码:

StepVerifier.create(Mono.just(1).map(i -> i + 10),
				StepVerifierOptions.create().withInitialContext(Context.of("foo", "bar"))) //有初始Context
		            .expectAccessibleContext() //上下文传播expectations
		            .contains("foo", "bar") //上下文中,key foo的值是bar
		            .then() //切换回正常expectations
		            .expectNext(11)
		            .verifyComplete();//结束 

Manually Emitting with TestPublisher

对于更高级的测试用例,可能需要完全掌握数据源,以便触发想要的信号。
或者你实现了自己的operator,想验证是否满足Reactive Streams规范。

对此,提供了TestPublisher类。它是Publisher ,可以用程序触发以下信号:

  • next(T)、next(T, T…):触发1-n个onNext信号
  • emit(T…) :发射数据,然后complete()
  • complete() :使用onComplete信号结束
  • error(Throwable):使用onError信号结束

可以通过create工厂方法获得TestPublisher。也可以使用createNonCompliant工厂方法,它从TestPublisher.Violation枚举中接受一个值或者多个值。这些值规定了发布者可以忽略规范的哪些部分:

  • REQUEST_OVERFLOW:即使请求不足,也允许调用next,不会抛IllegalStateException
  • ALLOW_NULL:对于null,也允许调用next,不会抛NullPointerException
  • CLEANUP_ON_TERMINATE:允许发送多次终止信号。包括complete()、 error()和emit()
  • DEFER_CANCELLATION:允许忽略cancel信号,继续发送信号

TestPublisher会在订阅以后跟踪内部状态,以使用assert*方法。

Checking the Execution Path with PublisherProbe

当构建复杂operators链的时候,可能会有多个执行路径,对应不同的子序列。
大多数情况下,这些子序列能够生产足够的onNext信号,可以执行到最后。
比如下面的例子,如果源是空的,就使用switchIfEmpty:

public Flux<String> processOrFallback(Mono<String> source, Publisher<String> fallback) {
    return source
            .flatMapMany(phrase -> Flux.fromArray(phrase.split("\\s+")))
            .switchIfEmpty(fallback);
}

很容易测试switchIfEmpty对应的逻辑分支:

@Test
public void testSplitPathIsUsed() {
    StepVerifier.create(processOrFallback(Mono.just("just a  phrase with    tabs!"), Mono.just("EMPTY_PHRASE")))
                .expectNext("just", "a", "phrase", "with", "tabs!")
                .verifyComplete();
}

@Test
public void testEmptyPathIsUsed() {
    StepVerifier.create(processOrFallback(Mono.empty(), Mono.just("EMPTY_PHRASE")))
                .expectNext("EMPTY_PHRASE")
                .verifyComplete();
}

但是,考虑另一个例子,方法生产的是Mono。它等待源完成,执行附加任务,然后完成。如果源是空的,执行Runnable类型的任务:

private Mono<String> executeCommand(String command) {
    return Mono.just(command + " DONE");
}

public Mono<Void> processOrFallback(Mono<String> commandSource, Mono<Void> doWhenEmpty) {
    return commandSource
            .flatMap(command -> executeCommand(command).then()) //then()忘记了command的返回。它只关心已经完成了
            .switchIfEmpty(doWhenEmpty); //如何区分两个都是空序列的情况
}

为了验证processOrFallback确实执行了doWhenEmpty路径,你需要写boilerplate。就是说,你需要一个Mono :

  • 它确实被订阅了
  • 整个处理结束后,断言该事实

在 3.1之前,你需要为每个想要断言的状态,手工维护一个AtomicBoolean,并将相应的doOn*回调附加到要评估的发布者。从3.1.0开始,可以使用PublisherProbe:

@Test
public void testCommandEmptyPathIsUsed() {
    PublisherProbe<Void> probe = PublisherProbe.empty(); //翻译空序列

    StepVerifier.create(processOrFallback(Mono.empty(), probe.mono())) //使用probe.mono()代替Mono<Void> 
                .verifyComplete();

    probe.assertWasSubscribed(); //序列完成后,确保使用了probe
    probe.assertWasRequested(); //以及实际请求的数据
    probe.assertWasNotCancelled(); //没有被取消
}

本方法,对Flux< T >也有效。对于需要探测执行路径,也需要探测数据的情况,你可以使用PublisherProbe.of(Publisher)包装任何Publisher< T >。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值