测试RxJava

重要要点

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

本文已更新,以适应出现的新变化和挑战。 访问“ Testing RXJava2 ”以获取较新版本。

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

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

第一步

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

import rx.Observable;
import rx.observers.TestSubscriber;
import rx.plugins.RxJavaHooks;
import rx.schedulers.Schedulers;
import java.util.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.*;
import org.junit.Test;
import static org.junit.Assert.assertThat;

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

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

@Test
public void testInSameThread() {
    // given:
    List<String> results = new ArrayList<>();
    Observable<String> observable = Observable
       .from(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提供了在这种情况下可以使用的TestSubscriber类型。 让我们使用这种类型重构先前的测试

@Test
public void testUsingTestSubscriber() {
    // given:
    TestSubscriber<String> subscriber = new TestSubscriber<>();

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

    // when:
    observable.subscribe(subscriber);

    // then:
    subscriber.assertCompleted();
    subscriber.assertNoErrors();
    subscriber.assertValueCount(9);
    assertThat(subscriber.getOnNextEvents(), hasItem(" 4. fox"));
}

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

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

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

    // when:
    observable.subscribe(subscriber);

    // then:
    subscriber.assertError(exception);
    subscriber.assertNotCompleted();
}

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

自订排程器

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

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

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

    // then:
    subscriber.assertCompleted();
    subscriber.assertNoErrors();
    assertThat(subscriber.getOnNextEvents(), hasItem(" 4. fox"));
}

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

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

  • Observable变为阻塞的Observable 。
  • 强制测试等待直到满足特定条件。
  • 将计算调度程序立即切换。

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

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

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

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

    // then:
    assertThat(results, notNullValue());
    assertThat(results, iterableWithSize(9));
    assertThat(results, 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() { 
3      // given: 
4      TestSubscriber<String> subscriber = new TestSubscriber<>(); 
5      Observable<String> observable = Observable.from(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(subscriber); 
12   
13     await().timeout(2, SECONDS) 
14            .until(subscriber::getValueCount, equalTo(9)); 
15   
16     // then: 
17     subscriber.assertCompleted(); 
18     subscriber.assertNoErrors(); 
19     assertThat(subscriber.getOnNextEvents(), hasItem(" 4. fox")); 
20   }

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

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

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

1    @Test 
2    public void testUsingRxJavaHooksWithImmediateScheduler() { 
3      // given: 
4      RxJavaHooks.setOnComputationScheduler(scheduler -> Schedulers.immediate()); 
5      TestSubscriber<String> subscriber = new TestSubscriber<>(); 
6      Observable<String> observable = Observable.from(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(subscriber); 
14   
15        // then: 
16        subscriber.assertCompleted(); 
17        subscriber.assertNoErrors(); 
18        subscriber.assertValueCount(9); 
19        assertThat(subscriber.getOnNextEvents(), hasItem(" 4. fox")); 
20    } finally { 
21        RxJavaHooks.reset(); 
22    } 
23   }

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

public class ImmediateSchedulersRule implements TestRule {
    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                RxJavaHooks
                    .setOnIOScheduler(scheduler -> Schedulers.immediate());
                RxJavaHooks
                    .setOnComputationScheduler(scheduler -> Schedulers.immediate());
                RxJavaHooks
                    .setOnNewThreadScheduler(scheduler -> Schedulers.immediate());
                try {
                     base.evaluate();
                } finally {
                     RxJavaHooks.reset(); }
                }
        };
    }
}

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

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

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

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

    // then:
    subscriber.assertNoErrors();
    subscriber.assertCompleted();
    subscriber.assertValueCount(9);
    assertThat(subscriber.getOnNextEvents(), hasItem(" 4. fox"));
}

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

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

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

玩时间

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

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

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

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

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

正如你可以看到使用这个调度是很方便,但它需要你调度提供给可观察下测试。 这对于使用特定调度程序的Observable不能很好地发挥作用。 但是,稍等片刻,我们前面已经看到了如何使用RxJavaHooks在不影响生产代码的情况下切换调度程序,但是这次提供了TestScheduler(第13-15行)而不是立即调度程序。 我们甚至可以应用自定义JUnit规则的相同技术,从而允许以更可重用的形式重写以前的代码。 首先是新规则:

1    public class TestSchedulerRule implements TestRule { 
2     private final TestScheduler testScheduler = new TestScheduler(); 
3    
4     public TestScheduler getTestScheduler() { 
5       return testScheduler; 
6     } 
7    
8     @Override 
9     public Statement apply(final Statement base, Description description) { 
10      return new Statement() { 
11        @Override 
12        public void evaluate() throws Throwable { 
13          RxJavaHooks.setOnIOScheduler(scheduler -> testScheduler); 
14          RxJavaHooks.setOnComputationScheduler(scheduler -> testScheduler); 
15          RxJavaHooks.setOnNewThreadScheduler(scheduler -> testScheduler); 
16   
17          try { base.evaluate(); } 
18          finally { RxJavaHooks.reset(); } 
19        } 
20      }; 
21    } 
22   }

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

23    @Rule 
24    public final TestSchedulerRule testSchedulerRule = new TestSchedulerRule(); 
25   
26    @Test 
27    public void testUsingTestSchedulersRule() { 
28      // given: 
29      TestSubscriber<String> subscriber = new TestSubscriber<>(); 
30   
31      Observable<String> observable = Observable.from(WORDS) 
32        .zipWith(Observable.interval(1, SECONDS), 
33          (string, index) -> String.format("%2d. %s", index, string)); 
34   
35      observable.subscribeOn(Schedulers.computation()) 
36        .subscribe(subscriber); 
37   
38      // expect 
39      subscriber.assertNoValues(); 
40      subscriber.assertNotCompleted(); 
41   
42      // when: 
43      testSchedulerRule.getTestScheduler().advanceTimeBy(1, SECONDS); 
44   
45      // then: 
46      subscriber.assertNoErrors(); 
47      subscriber.assertValueCount(1); 
48      subscriber.assertValues(" 0. the"); 
49   
50      // when: 
51      testSchedulerRule.getTestScheduler().advanceTimeTo(9, SECONDS); 
52      subscriber.assertCompleted(); 
53      subscriber.assertNoErrors(); 
54      subscriber.assertValueCount(9); 
55    }

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

未来

RxJava是最早为Java提供React式编程功能的库之一。 即将推出的2.0版已经过重新设计,以使其API与Reactive Streams规范更好地保持一致,该规范为非阻塞背压的异步流处理提供了一个标准,以Java和JavaScript运行时为目标。 这意味着在下一版本中将有一些API更改; 您可以在RxJava Wiki上找到这些更改的详细说明。

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

本文已更新,以适应出现的新变化和挑战。 访问“ Testing RXJava2 ”以获取较新版本。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值