测试rxjava2_测试RxJava2

测试rxjava2

重要要点

  • RxJava 2包括内置的,易于测试的解决方案。
  • 使用TestSubscriber验证Flowable,使用TestObserver验证Observable,Single,Maybes和Completable。
  • 使用TestScheduler可以严格控制时间。
  • Awaitility库提供了对测试上下文的附加控制。

本文是“ Testing RXJava ”的修订版,已完全更新以符合RxJava2规范。

您已经了解了RxJava; 您已经在Internet上玩过这些示例,例如在Example中的RxJava玩过 ,现在您已承诺在自己的代码中探索响应机会。 但是现在您想知道如何测试代码库中可能提供的新功能。

响应式编程要求对特定问题的推理方式有所转变,因为我们不需要将重点放在单个数据项上,而应关注作为事件流流动的数据。 这些事件通常是从不同的线程产生和使用的,因此在编写测试时,我们必须注意并发问题。 幸运的是,RxJava2为测试Observable和Disposable提供了内置支持,这些支持内置在核心rxjava依赖项中。

第一步

让我们重新回顾上一篇文章中的单词示例,并探讨如何进行测试。 让我们首先使用JUnit作为我们的测试框架来设置基本测试工具:

import io.reactivex.Observable;
import io.reactivex.observers.TestObserver;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import org.junit.Test;
import java.util.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.*;


public class RxJavaTest {
    private static final List<String> WORDS = Arrays.asList(
       "the",
       "quick",
       "brown",
       "fox",
       "jumped",
       "over",
       "the",
       "lazy",
       "dog"
    );
}

考虑到默认情况下,如果未提供特定的Scheduler,则订阅将默认在调用线程上运行,因此我们的第一个测试将采用幼稚的方法。 这意味着我们可以设置订阅并在订阅发生后立即声明其状态:

@Test
public void testInSameThread() {
   // given:
   List<String> results = new ArrayList<>();
   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string));

   // when:
   observable.subscribe(results::add);

   // then:
   assertThat(results, notNullValue());
   assertThat(results, hasSize(9));
   assertThat(results, hasItem(" 4. fox"));

注意,我们使用了一个显式的List <String>来累加我们的结果以及一个真实的订阅者。 考虑到此测试的简单性,您可能认为使用显式累加器就足够了,但是请记住,生产级可观察变量可能会封装错误或产生意外事件; 简单的订户加累加器组合不足以涵盖这些情况。 但是不要担心,RxJava提供了在这种情况下可以使用的TestObserver类型。 让我们使用这种类型重构先前的测试

@Test
public void testUsingTestObserver() {
   // given:
   TestObserver<String> observer = new TestObserver<>();
   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string));

   // when:
   observable.subscribe(observer);

   // then:
   observer.assertComplete();
   observer.assertNoErrors();
   observer.assertValueCount(9);
   assertThat(observer.values(), hasItem(" 4. fox"));
}

TestObserver替代了自定义累加器,但它还提供了一些其他行为。 例如,它能够告诉我们收到了多少个事件以及与每个事件相关的数据。 它还可以断言订阅已成功完成,并且在使用Observable期间没有出现任何错误。 当前的受测Observable不会产生任何错误,但是正如我们在Example中观察到的在RxJava中观察到的,异常与数据事件完全相同。 我们可以通过以下方式串联异常事件来模拟错误

@Test
public void testFailure() {
   // given:
   TestObserver<String> observer = new TestObserver<>();
   Exception exception = new RuntimeException("boom!");

   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string))
       .concatWith(Observable.error(exception));

   // when:
   observable.subscribe(observer);

   // then:
   observer.assertError(exception);
   observer.assertNotComplete();
}

在我们有限的用例中,一切都很好。 但是实际的生产代码可能相差很大,因此让我们考虑一些更复杂的生产案例。

自订排程器

通常,您会在生产代码中找到在特定线程上执行Observable的情况,或者在Rx措辞中使用“调度程序”。 许多Observable操作将可选的Scheduler参数作为附加参数。 RxJava定义了一组可以随时使用的命名调度程序。 其中一些是io和计算(它们是共享线程)和newThread。 您还可以提供自己的自定义Scheduler实现。 我们通过指定计算调度程序来更改可观察的代码。

@Test
public void testUsingComputationScheduler() {
   // given:
   TestObserver<String> observer = new TestObserver<>();
   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string));

   // when:
   observable.subscribeOn(Schedulers.computation())
       .subscribe(observer);

   await().timeout(2, SECONDS)
       .until(observer::valueCount, equalTo(9));

   // then:
   observer.assertComplete();
   observer.assertNoErrors();
   assertThat(observer.values(), hasItem(" 4. fox"));
}

运行该测试后,您会很快发现该测试存在问题。 订户在测试线程上执行其声明,但是Observable在后台线程(计算线程)上生成值。 这意味着可以在Observable产生所有相关事件之前执行订阅者的断言,从而导致测试失败。

我们可以选择几种策略来使测试变为绿色:

  • 将Observable变成一个障碍。
  • 强制测试等待直到满足特定条件。
  • 将计算调度程序立即切换。

我们将从需要较少工作的策略开始介绍每种策略:将Observable变为阻塞Observable。 无论使用什么调度程序,此技术都有效。 假定数据是在后台线程中产生的,从而导致在同一后台线程中通知订户。

我们要执行的是在评估测试中的下一条语句之前,强制所有事件都生成并完成Observable。 这是通过在Observable本身上调用blockingIterable()完成的。

@Test
public void testUsingBlockingCall() {
   // given:
   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string));

   // when:
   Iterable<String> results = observable
       .subscribeOn(Schedulers.computation())
       .blockingIterable();

   // then:
   assertThat(results, notNullValue());
   assertThat(results, iterableWithSize(9));
   assertThat(results, hasItem(" 4. fox"));
}

虽然这种方法对于我们展示的普通代码可能是可以接受的,但对于实际的生产代码可能并不实际。 如果生产者花费很长时间创建所有数据怎么办? 这将使测试变慢,从而增加了构建时间。 可能还有其他计时问题。 幸运的是,TestObserver提供了一组可以强制测试等待终止事件的方法。 这是可以完成的方法:

@Test
	    public void testUsingComputationScheduler() {
    	// given:
    	TestObserver<String> observer = new TestObserver<>();
    	Observable<String> observable = Observable.fromIterable(WORDS)
        	.zipWith(Observable.range(1, Integer.MAX_VALUE),
            	(string, index) -> String.format("%2d. %s", index, string));

    	// when:
    	observable.subscribeOn(Schedulers.computation())
        	.subscribe(observer);

    	observer.awaitTerminalEvent(2, SECONDS);

    	// then:
    	observer.assertComplete();
    	observer.assertNoErrors();
    	assertThat(observer.values(), hasItem(" 4. fox"));
	    }

如果那还不够,我现在想把您的注意力转移到一个名为Awaitility的方便的库中。 简而言之,Awaitility是一种DSL,可让您以简洁易懂的方式表达对异步系统的期望。 您可以使用Maven包括Awaitility依赖项:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>2.0.0</version>
    <scope>test</scope>
</dependency>

或使用Gradle:

testCompile 'org.awaitility:awaitility:2.0.0'

Awaitility DSL的入口点是org.awaitility.Awaitility.await()方法(请参见下面示例中的第13-14行)。 从那里可以定义必须满足的条件才能继续测试。 您可以用超时和其他时间限制(例如最小,最大或持续时间范围)来修饰条件。 在拖曳中使用Awaitility重新访问先前的测试会导致以下代码

1 @Test
2 public void testUsingComputationScheduler_awaitility() {
3     // given:
4     TestObserver<String> observer = new TestObserver<>();
5     Observable<String> observable = Observable.fromIterable(WORDS)
6         .zipWith(Observable.range(1, Integer.MAX_VALUE),
7             (string, index) -> String.format("%2d. %s", index, string));
8
9     // when:
10     observable.subscribeOn(Schedulers.computation())
11         .subscribe(observer);
12
13     await().timeout(2, SECONDS)
14         .until(observer::valueCount, equalTo(9));
15
16     // then:
17     observer.assertComplete();
18     observer.assertNoErrors();
19     assertThat(observer.values(), hasItem(" 4. fox"));
20 }

此版本不会以任何方式更改Observable的性质,这使您无需进行任何修改即可测试未更改的生产代码。 此版本的测试最多等待2秒钟,以便Observable通过检查订户的状态来执行其工作。 如果一切顺利,则在2秒钟超时之前,订户的状态将检出9个事件。

Awaitility与Hamcrest匹配器,Java 8 lambda和方法引用配合得很好,因此产生了简洁易读的条件。 还提供了流行的JVM语言(例如Groovy和Scala)的现成扩展。

我们将介绍的最终策略是利用RxJava作为其API的一部分公开的扩展机制。 RxJava定义了一系列扩展点,使您可以调整其默认行为的几乎每个方面。 这种扩展机制有效地使我们能够为特定的RxJava功能提供量身定制的值。 我们将利用这种机制让我们的测试注入特定的Scheduler,而不受生产代码指定的调度程序的影响。 我们正在寻找的行为封装在RxJavaPlugins类中。 假设我们的生产代码依赖于Calculation()调度程序,我们将覆盖其默认值,返回一个调度程序,使事件处理与调用方代码在同一线程中进行; 这是Schedulers.trampoline()调度程序。 现在是测试的样子:

1 @Test
2 public void testUsingRxJavaPluginsWithImmediateScheduler() {
3     // given:
4     RxJavaPlugins.setComputationSchedulerHandler(scheduler -> 
                                                   Schedulers.trampoline());
5     TestObserver<String> observer = new TestObserver<>();
6     Observable<String> observable = Observable.fromIterable(WORDS)
7         .zipWith(Observable.range(1, Integer.MAX_VALUE),
8             (string, index) -> String.format("%2d. %s", index, string));
9
10     try {
11         // when:
12         observable.subscribeOn(Schedulers.computation())
13             .subscribe(observer);
14
15         // then:
16         observer.assertComplete();
17         observer.assertNoErrors();
18         observer.assertValueCount(9);
19         assertThat(observer.values(), hasItem(" 4. fox"));
20     } finally {
21         RxJavaPlugins.reset();
22     }
23 }

生产代码不知道在测试过程中Calculation()调度程序是立即执行的。 请注意,您必须重置挂钩,否则立即调度程序设置可能会泄漏,从而导致整个地方的测试都被破坏。 使用try / finally块会使测试代码的意图有些模糊,但是幸运的是,我们可以使用JUnit规则来重构此行为,从而使测试更苗条并且更易读。 这是这种规则的一种可能的实现

private static class ImmediateSchedulersRule implements TestRule {
   @Override
   public Statement apply(final Statement base, Description description) {
       return new Statement() {
           @Override
           public void evaluate() throws Throwable {
               RxJavaPlugins.setIoSchedulerHandler(scheduler -> 
                                                Schedulers.trampoline());
               RxJavaPlugins.setComputationSchedulerHandler(scheduler -> 
                                                Schedulers.trampoline());
               RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> 
                                                Schedulers.trampoline());

               try {
                   base.evaluate();
               } finally {
                   RxJavaPlugins.reset();
               }
           }
       };
   }
}

为了更好地衡量,我们重写了其他两种调度程序生成方法,使此规则对于以后的其他测试目的更加通用。 在新的测试用例类中使用此规则很简单,我们只需声明一个带有@Rule注释的新类型的字段,就像这样

@Rule
public final ImmediateSchedulersRule schedulers = 
                                         new ImmediateSchedulersRule();

@Test
public void testUsingImmediateSchedulersRule() {
   // given:
   TestObserver<String> observer = new TestObserver<>();
   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.range(1, Integer.MAX_VALUE),
           (string, index) -> String.format("%2d. %s", index, string));

   // when:
   observable.subscribeOn(Schedulers.computation())
       .subscribe(observer);

   // then:
   observer.assertComplete();
   observer.assertNoErrors();
   observer.assertValueCount(9);
   assertThat(observer.values(), hasItem(" 4. fox"));
}

最后,我们获得了与以前相同的行为,但杂波更少。 让我们花点时间回顾一下到目前为止我们已经完成的成就:

  • 只要没有特定的用途,订阅服务器就会在同一线程中处理数据。 这意味着我们可以在订户订阅之后立即对其进行断言。
  • TestObserver可以累积事件并提供有关其状态的其他声明。
  • 任何事件都可以变成一个阻塞,从而使我们能够同步等待事件发生,而不管可观察对象使用了什么事件。
  • RxJava公开了一种扩展机制,使开发人员可以覆盖其默认值,并将这些默认值注入生产代码中。
  • 可以使用Awaitility使用DSL测试并发代码。

这些技术中的每一种在不同的场景中都派上用场,但是所有这些技术都通过一个公共线程(双关语)连接:测试代码在对订户的状态进行声明之前,等待Observable完成。 如果在生成数据时有一种方法来检查可观察对象的行为该怎么办? 换句话说,如果可以通过编程方式调试Observable,该怎么办? 接下来,我们将介绍一种技术。

玩时间

到目前为止,我们已经以黑盒方式测试了可观察性和订阅。 现在,我们将介绍另一种技术,该技术允许我们以某种方式操纵时间,以便在Observable仍处于活动状态时可以打开引擎盖并查看订户的状态,换句话说,我们将使用白框测试技术。 再一次,它的TestScheduler类使RxJava脱颖而出。 使用此特定的Scheduler,您可以准确地指定时间在其中的经过时间。 例如,您可以将时间提前半秒,或使其跃过5秒。 我们将从创建此新Scheduler的实例开始,然后将其传递给测试代码

1 @Test
2 public void testUsingTestScheduler() {
3     // given:
4     TestScheduler scheduler = new TestScheduler();
5     TestObserver<String> observer = new TestObserver<>();
6     Observable<Long> tick = Observable.interval(1, SECONDS, scheduler);
7
8     Observable<String> observable = Observable.fromIterable(WORDS)
9         .zipWith(tick,
10             (string, index) -> String.format("%2d. %s", index, string));
11
12     observable.subscribeOn(scheduler)
13         .subscribe(observer);
14
15     // expect:
16     observer.assertNoValues();
17     observer.assertNotComplete();
18
19     // when:
20     scheduler.advanceTimeBy(1, SECONDS);
21
22     // then:
23     observer.assertNoErrors();
24     observer.assertValueCount(1);
25     observer.assertValues(" 0. the");
26
27     // when:
28     scheduler.advanceTimeTo(9, SECONDS);
29     observer.assertComplete();
30     observer.assertNoErrors();
31     observer.assertValueCount(9);
32 }

“生产”代码发生了一些变化,因为我们现在使用的是与调度程序绑定的间隔来产生编号(第6行)而不是范围。 这具有产生从0开始而不是原始1的数字的副作用。一旦配置了Observable和测试调度程序,我们立即断言订户没有值(第16行),并且未完成或未产生任何错误(第17行)。 这是一个健全性检查,因为调度程序此时尚未移动,因此观察者不应生成任何值,订户也不应接收任何值。

接下来,我们将时间移动一整秒(第20行),这将使Observable产生第一个值,这正是下一组断言检查的内容(第23-25行)。

接下来,我们将时间从现在缩短到9秒。 请注意,这意味着从调度程序的开始移至恰好9秒(而不是在已经提前1的情况下提前9秒钟,这将导致调度程序在启动后的10秒内查看)。 换句话说,advanceTimeBy()相对于其当前位置移动调度程序的时间,而advanceTimeTo()以绝对方式移动调度程序的时间。 我们进行另一轮断言(第29-31行),以确保Observable已产生所有数据,并且订户也已使用了所有数据。 关于TestScheduler的用法,还有一点要注意的是实时会立即移动,这意味着我们的测试不必等待9秒钟即可完成。

如您所见,使用此调度程序非常方便,但需要将调度程序提供给被测对象。 这对于使用特定调度程序的Observable不能很好地发挥作用。 但是,稍等片刻,我们前面已经看到了如何使用RxJavaPlugins在不影响生产代码的情况下切换调度程序,但是这次提供了TestScheduler而不是立即调度程序。 我们甚至可以应用自定义JUnit规则的相同技术,从而允许以更可重用的形式重写以前的代码。 首先是新规则:

private static class TestSchedulerRule implements TestRule {
   private final TestScheduler testScheduler = new TestScheduler();

   public TestScheduler getTestScheduler() {
       return testScheduler;
   }

   @Override
   public Statement apply(final Statement base, Description description) {
       return new Statement() {
           @Override
           public void evaluate() throws Throwable {
               RxJavaPlugins.setIoSchedulerHandler(scheduler -> 
                                                   testScheduler);
               RxJavaPlugins.setComputationSchedulerHandler(scheduler -> 
                                                   testScheduler);
               RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> 
                                                   testScheduler);

               try {
                   base.evaluate();
               } finally {
                   RxJavaPlugins.reset();
               }
           }
       };
   }
}

接下来是实际的测试代码(在新的测试用例类中),以使用我们的测试规则:

@Rule
public final TestSchedulerRule testSchedulerRule = new TestSchedulerRule();

@Test
public void testUsingTestSchedulersRule() {
   // given:
   TestObserver<String> observer = new TestObserver<>();

   Observable<String> observable = Observable.fromIterable(WORDS)
       .zipWith(Observable.interval(1, SECONDS),
           (string, index) -> String.format("%2d. %s", index, string));

   observable.subscribeOn(Schedulers.computation())
       .subscribe(observer);

   // expect
   observer.assertNoValues();
   observer.assertNotComplete();

   // when:
   testSchedulerRule.getTestScheduler().advanceTimeBy(1, SECONDS);

   // then:
   observer.assertNoErrors();
   observer.assertValueCount(1);
   observer.assertValues(" 0. the");

   // when:
   testSchedulerRule.getTestScheduler().advanceTimeTo(9, SECONDS);
   observer.assertComplete();
   observer.assertNoErrors();
   observer.assertValueCount(9);
}

那里有。 通过RxJavaPlugins注入的TestScheduler的使用使您可以编写测试代码,而无需更改原始Observable的组成,但它为您提供了在Observable本身执行期间修改时间并在特定点进行声明的方法。 本文显示的所有技术都应为您提供足够的选择来测试启用RxJava的代码。

未来

RxJava是最早为Java提供React式编程功能的库之一。 2.0版已经过重新设计,以使其API与Reactive Streams规范更好地保持一致,该规范提供了针对Java和JavaScript运行时的无阻塞背压的异步流处理的标准。 我强烈建议您查看自2.0版以来的API更改; 您可以在RxJava Wiki上找到这些更改的详细说明。

在测试方面,您会看到核心类型(Observable,Maybe和Single)现在可以使用名为test()的便捷方法,该方法可以为您现场创建TestObserver实例。 现在,您可以在TestObserver上链接方法调用,并且在此类型上也找到了一些新的断言方法。

这是“ Testing RXJava ”的更新版本

关于作者

是Java / Groovy开发人员和Java Champion,在软件设计和开发方面拥有17年以上的经验。 自Java成立以来,他就一直从事Web和桌面应用程序开发。 他是开源的忠实拥护者,并参与了Groovy,JMatter,Ascidoctor等热门项目。 Griffon框架的创始成员和当前项目负责人。 JSR 377的规范线。

翻译自: https://www.infoq.com/articles/Testing-RxJava2/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

测试rxjava2

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值