重要要点
|
本文已更新,以适应出现的新变化和挑战。 访问“ Testing RXJava2 ”以获取较新版本。
您已经了解了RxJava; 您已经在Internet上玩过这些示例,例如在Example中的RxJava中玩过 ,现在您已承诺在自己的代码中探索响应机会。 但是现在您想知道如何测试代码库中可能提供的新功能。
响应式编程要求对特定问题的推理方式有所转变,因为我们不需要将重点放在单个数据项上,而应关注作为事件流流动的数据。 这些事件通常是从不同的线程产生和使用的,因此在编写测试时,我们必须注意并发问题。 幸运的是,RxJava提供了对测试Observables
和Subscriptions
内置支持,内置在核心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, Maybe
和Single
)现在具有名为test()
的便捷方法,该方法可以为您现场创建TestSubscriber实例。 现在,您可以在TestSubscriber
上链接方法调用,并且在此类型上也可以找到一些新的断言方法。
本文已更新,以适应出现的新变化和挑战。 访问“ Testing RXJava2 ”以获取较新版本。