有专属的测试包:
<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 >。