当前位置: 首页 > news >正文

(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*方法来跟踪其内部的订阅和执行状态。

转载于:https://blog.51cto.com/liukang/2094939

相关文章:

  • Oracle 谈 JavaFX 及 Java 客户端技术的未来
  • [译] React 中的受控组件和非受控组件
  • drbd配置简述
  • 聊聊编译时注解
  • 微服务架构—高级设计篇
  • 漫谈版本控制系统
  • pandas(一)操作Series和DataFrame的基本功能
  • Redhat6.5匿名访问win10共享文件夹.
  • 从零开始在ubuntu上搭建node开发环境
  • 《CDN 之我见》原理篇——CDN的由来与调度
  • 汇编语言第三版答案(王爽)
  • RSACryptoServiceProvider加密解密签名验签和DESCryptoServiceProvider加解密
  • 算法之不定期更新(一)(2018-04-12)
  • Java一行代码控制shape 优雅的解决 Drawable Shape 文件繁多问题
  • innerWidth outerWidth
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • Android组件 - 收藏集 - 掘金
  • C++类的相互关联
  • exports和module.exports
  • JavaScript创建对象的四种方式
  • java第三方包学习之lombok
  • Java面向对象及其三大特征
  • JWT究竟是什么呢?
  • Lsb图片隐写
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • redis学习笔记(三):列表、集合、有序集合
  • Vue--数据传输
  • 从0实现一个tiny react(三)生命周期
  • 力扣(LeetCode)56
  • 码农张的Bug人生 - 初来乍到
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 猫头鹰的深夜翻译:JDK9 NotNullOrElse方法
  • 区块链技术特点之去中心化特性
  • 微信开放平台全网发布【失败】的几点排查方法
  • 译有关态射的一切
  • 主流的CSS水平和垂直居中技术大全
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • 函数计算新功能-----支持C#函数
  • ​LeetCode解法汇总518. 零钱兑换 II
  • #pragma 指令
  • $L^p$ 调和函数恒为零
  • (007)XHTML文档之标题——h1~h6
  • (1)(1.13) SiK无线电高级配置(五)
  • (27)4.8 习题课
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (ros//EnvironmentVariables)ros环境变量
  • (SpringBoot)第七章:SpringBoot日志文件
  • (zt)基于Facebook和Flash平台的应用架构解析
  • (二)linux使用docker容器运行mysql
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (强烈推荐)移动端音视频从零到上手(下)
  • (三十五)大数据实战——Superset可视化平台搭建
  • (转)ABI是什么
  • (转)为C# Windows服务添加安装程序
  • .bat文件调用java类的main方法