(16)Reactor的測試——響應式Spring的道法術器

本系列文章索引《響應式Spring的道法術器》
前情提要:Reactor 3快速上手 | 響應式流規範
本文測試源碼

2.6 測試

在非常重視DevOps的今天,以及一些奉行TDD的團隊中,自動化測試是保證代碼質量的重要手段。要進行Reactor的測試,首先要確保添加reactor-test依賴。

reactor-test 用 Maven 配置 <dependencies>

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

reactor-test 用 Gradle 配置

dependencies {
testCompile 'io.projectreactor:reactor-test:3.1.4.RELEASE'
}

2.6.1 使用 StepVerifier 來測試

1.3.2.3節初步介紹了關於StepVerifier的用法。舉個例子回憶一下:

    @Test
    public void testAppendBoomError() {
        Flux<String> source = Flux.just("foo", "bar");

        StepVerifier.create(
                appendBoomError(source))
                .expectNext("foo")
                .expectNext("bar")
                .expectErrorMessage("boom")
                .verify();
    }

我們通常使用create方法創建基於Flux或Mono的StepVerifier,然後就可以進行以下測試:

  • 測試期望發出的下一個信號。如果收到其他信號(或者信號與期望不匹配),整個測試就會 失敗(AssertionError),如expectNext(T...)expectNextCount(long)。`
  • 處理(consume)下一個信號。當你想要跳過部分序列或者當你想對信號內容進行自定義的校驗的時候會用到它,可以使用consumeNextWith(Consumer&lt;T&gt;)
  • 其他操作,比如暫停或運行一段代碼。比如,你想對測試狀態或內容進行調整或處理, 你可能會用到thenAwait(Duration)then(Runnable)

對於終止事件,相應的期望方法(如expectComplete()expectError(),及其所有的變體方法) 使用之後就不能再繼續增加別的期望方法了。最後你只能對 StepVerifier 進行一些額外的配置並 觸發校驗(通常調用verify()及其變體方法)。

StepVerifier內部實現來看,它訂閱了待測試的 Flux 或 Mono,然後將序列中的每個信號與測試 場景的期望進行比對。如果匹配的話,測試成功。如果有不匹配的情況,則拋出AssertionError異常。

響應式流是一種基於時間的數據流。許多時候,待測試的數據流存在延遲,從而持續一段時間。如果這種場景比較多的話,那麼會導致自動化測試運行時間較長。因此StepVerifier提供了可以操作“虛擬時間”的測試方式,這時候需要使用StepVerifier.withVirtualTime來構造。

爲了提高 StepVerifier 正常起作用的概率,它一般不接收一個簡單的 Flux 作爲輸入,而是接收 一個Supplier,從而可以在配置好訂閱者之後 “懶創建”待測試的 flux,如:

StepVerifier.withVirtualTime(() -> Mono.delay(Duration.ofDays(1)))
//... 繼續追加期望方法

有兩種處理時間的期望方法,無論是否配置虛擬時間都是可用的:

  • thenAwait(Duration)會暫停校驗步驟(允許信號延遲發出)。
  • expectNoEvent(Duration)同樣讓序列持續一定的時間,期間如果有任何信號發出則測試失敗。

在普通的測試中,兩個方法都會基於給定的持續時間暫停線程的執行。而如果是在虛擬時間模式下就相應地使用虛擬時間。

    StepVerifier.withVirtualTime(() -> Mono.delay(Duration.ofDays(1)))
        .expectSubscription()   // 1
        .expectNoEvent(Duration.ofDays(1))  // 2
        .expectNext(0L)
        .verifyComplete();  // 3
  1. expectNoEvent 將訂閱(subscription)也認作一個事件。假設你用它作爲第一步,如果檢測 到有訂閱信號,也會失敗。這時候可以使用expectSubscription().expectNoEvent(duration) 來代替;
  2. 期待“一天”內沒有信號發生;
  3. verify或變體方法最終會返回一個Duration,這是實際的測試時長。

可見,withVirtualTime使我們不用實際等1天來完成測試了。

虛擬時間的功能是如何實現的呢?StepVerifier.withVirtualTime會在Reactor的調度器工廠方法中插入一個自定義的調度器VirtualTimeScheduler來代替默認調度器(那些基於時間的操作符通常默認使用Schedulers.parallel()調度器)。

2.6.2 用 PublisherProbe 檢查執行路徑

通常情況下,使用StepVerifierexpect*就可以搞定多數的測試場景了。但是,它也有無計可施的時候,比如下邊這個特殊的例子:

    private Mono<String> executeCommand(String command) {
        // 基於command執行一些操作,執行完成後返回Mono<String>
    }

    public Mono<Void> processOrFallback(Mono<String> commandSource, Mono<Void> doWhenEmpty) {
        return commandSource
                .flatMap(command -> executeCommand(command).then())     // 1
                .switchIfEmpty(doWhenEmpty);    // 2
    }
  1. then()會忽略所有的元素,只保留完成信號,所以返回值爲Mono<Void>;
  2. 也是一個Mono<Void>。

1和2都是Mono&lt;Void&gt;,這時候就比較難判斷processOfFallback中具體執行了哪條路徑。這時候可以用log()doOn*()等方法來觀察,但這“在非綠即紅”的單測中不起作用。或者在某個路徑加入標識狀態的值,並通過判斷狀態值是否正確來確定,但這就需要修改被測試的processOfFallback的代碼了。

Reactor版本 3.1.0 之後我們可以使用PublisherProbe來做類似場景的驗證。如下:

    @Test
    public void testWithPublisherProbe() {
        PublisherProbe<Void> probe = PublisherProbe.empty();    // 1

        StepVerifier.create(processOrFallback(Mono.empty(), probe.mono()))  // 2
                    .verifyComplete();

        probe.assertWasSubscribed();    // 3
        probe.assertWasRequested();     // 4
        probe.assertWasNotCancelled();  // 5
    }
  1. 創建一個探針(probe),它會轉化爲一個空序列。
  2. 在需要使用 Mono<Void> 的位置調用 probe.mono() 來替換爲探針。
  3. 序列結束之後,你可以用這個探針來判斷序列是如何使用的,你可以檢查是它從哪(條路徑)被訂閱的…​
  4. 對於請求也是一樣的…​
  5. 以及是否被取消了。

2.6.3 使用TestPublisher手動發出元素

TestPublisher本質上是一個Publisher,不過使用它能更加“自由奔放”地發出各種元素,以便進行各種場景的測試。

1)“自由”地發出元素

我們可以用它提供的方法發出各種信號:

  • next(T) 以及 next(T, T...) 發出 1-n 個 onNext 信號。
  • emit(T...) 起同樣作用,並且會執行 complete()。
  • complete() 會發出終止信號 onComplete。
  • error(Throwable) 會發出終止信號 onError。

比如:

    @Test
    public void testWithTestPublisher() {
        TestPublisher<Integer> testPublisher = TestPublisher.<Integer>create().emit(1, 2, 3);
        StepVerifier.create(testPublisher.flux().map(i -> i * i))
                .expectNext(1, 4, 9)
                .expectComplete();
    }

2)“奔放”地發出元素

使用create工廠方法就可以得到一個正常的TestPublisher。而使用createNonCompliant 工廠方法可以創建一個“不正常”的TestPublisher。後者需要傳入由TestPublisher.Violation 枚舉指定的一組選項,這些選項可用於告訴 publisher 忽略哪些問題。枚舉值有:

  • REQUEST_OVERFLOW: 允許 next 在請求不足的時候也可以調用,而不會觸發 IllegalStateException。
  • ALLOW_NULL: 允許 next 能夠發出一個 null 值而不會觸發 NullPointerException。
  • CLEANUP_ON_TERMINATE: 可以重複多次發出終止信號,包括 complete()、error() 和 emit()。

不過這個功能可能更多地是給Reactor項目開發者本身使用的,比如當他們開發了一個新的操作符,可以用這種方式來測試這個操作符是否滿足響應式流的規範。

3)TestPublisher也是個PublisherProbe

更讚的是,TestPublisher實現了PublisherProbe接口,意味着我們還可以使用它提供的assert*方法來跟蹤其內部的訂閱和執行狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章