接續上一篇介紹測試,之前也提到過Reactor提供VirtualTimeScheduler
來讓測試更方便,現在就來結合StepVerifier
與VirtualTimeScheduler
看看會有甚麼火花。
測試使用StepVerifier
傳入publisher
的方法,除了常用到的create
來接收之外,withVirtualTime
也是一個好方法,尤其是當你的情境是與時間有關的時候,從原始碼可以看到,有別於create
是直接傳入一個publisher
,withVirtualTime
需要用Supplier
包住publisher
後再傳入,原因是時間相關的operators通常是透過Schedulers.parallel()
,如果要使用虛擬時間就要用VirtualTimeScheduler
去取代掉原本的Scheduler
,所以VirtualTimeScheduler
必須在operators初始化之前就已經啟動,也就是要延後operators初始化的時間,這句話基本上就等於lazy loading,所以為了讓虛擬時間的機制成功,必須在Supplier
內初始化Flux
,而不是在其他的地方。
static <T> FirstStep<T> withVirtualTime(Supplier<? extends Publisher<? extends T>> scenarioSupplier) {
return withVirtualTime(scenarioSupplier, Long.MAX_VALUE);
}
StepVerifier
提供了兩個方法讓你可以控制時間增加
thenAwait(Duration)
:暫停期望評估(expectation evaluation),停下來等相對就是時間增加(可以順便反思人生不進則退,時光飛逝......)expectNoEvent(Duration)
:在傳入的Duration期間內預期沒有任何事情發生,相對也是一個時間推進的概念。要注意subscription
也被視做一個event,如果一開始預期就沒有event發生,建議使用expectSubscription().expectNoEvent(duration)
,要不然都會是錯誤。withVirtualTime
的同時必須要用到上述的兩種方法去增加時間,否則時間就將會停滯。從官方提供的範例可以看到,遇到delay
建議就使用expectSubscription
先起手,避免subscription
這個事件導致expectNoEvent
失敗。透過VirtualTime
推進了一天之後得到了一個資料0L,整個測試過程在真實世界只過了不到1秒。
StepVerifier.withVirtualTime(() -> Mono.delay(Duration.ofDays(1)))
.expectSubscription()
.expectNoEvent(Duration.ofDays(1))
.expectNext(0L)
.verifyComplete();
在研究的時候找到一個stackoverflow的提問,提問者說,為何他的test如果不加上timeout的限制,就永遠不會停止,而如果加上了則會有exception發生。
StepVerifier.withVirtualTime(() ->
Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)))
.expectSubscription()
.expectNextCount(4)
.expectComplete()
.verify(Duration.ofSeconds(10));
//java.lang.AssertionError: VerifySubscribertimed out on reactor.core.publisher.FluxConcatMap$ConcatMapImmediate@66d1af89
如果你有認真閱讀上面分享的內容並且吸收,你應該能看出問題點,為了避免暴雷,中間閒聊一下讓大家思考,我看完找出問題後往下確認,發現回答的人居然是Simon Baslé!也就是Project Reactor的頭頭,這是不是有點像是你問了一個萬有引力的問題,結果發現回答的人是牛頓。
公布答案!答案就是他完全沒有用到任何會增加時間的方法,順利成為了讓時間停滯的男/女人,在上面的內容有提到如何增加時間,往上找一個時鐘的圖案就能發現了,最後附上Project Reactor的頭頭的精采回答
You need to use .thenAwait(Duration), otherwise the (virtual) clock won't move, and the delay never happens. You can also use .expectNoEvent(Duration) after the expectSubscription().
For example:
@Test
public void test() {
StepVerifier.withVirtualTime(() ->
Flux.just(1, 2, 3, 4).delayElements(Duration.ofSeconds(1)))
.expectSubscription() //t == 0
//move the clock forward by 1s, and check nothing is emitted in the meantime
.expectNoEvent(Duration.ofSeconds(1))
//so this effectively verifies the first value is delayed by 1s:
.expectNext(1)
//and so on...
.expectNoEvent(Duration.ofSeconds(1))
.expectNext(2)
//or move the clock forward by 2s, allowing events to take place,
//and check last 2 values where delayed
.thenAwait(Duration.ofSeconds(2))
.expectNext(3, 4)
.expectComplete()
//trigger the verification and check that in realtime it ran in under 200ms
.verify(Duration.ofMillis(200));
}