隨着響應式編程RxJava這幾年的火熱,大家在項目中也會常常使用。RxJava提供了大量的操作符,讓我們的代碼顯得更簡潔,對於線程的切換也更加自如。那麼當我們寫單元測試時,如何方便的測試RxJava呢?這就是本篇的內容。
1.一個簡單的測試
首先添加一下依賴
//RxJava
compile 'io.reactivex.rxjava2:rxjava:2.1.7'
RxJava2提供了我們TestObserver
,它可以記錄事件並允許對它們進行斷言。
@Test
public void testObserver() {
TestObserver<Integer> testObserver = TestObserver.create();
testObserver.onNext(1);
testObserver.onNext(2);
//斷言值是否相等
testObserver.assertValues(1, 2);
testObserver.onComplete();
//斷言是否完成
testObserver.assertComplete();
}
在上面的代碼中,我們創建了TestObserver
,並且依次發射了1和2兩個數字。因爲可以記錄我們的操作事件並且斷言。所以我們很快的測試了我們想驗證的結果。
2.一些常用操作符的測試
同樣的RxJava內置了TestSubscriber
類,它可以記錄事件並允許對它作出斷言,那麼使用TestSubscriber
就可以去便捷的驗證Observable
。下面的內容很簡單,我也寫了對應的註釋。只要你會使用RxJava都可以快速的理解。
1.just
@Test
public void testJust() {
TestSubscriber<String> testSubscriber = new TestSubscriber<>();
//依次發射A,B,C
Flowable.just("A", "B", "C").subscribe(testSubscriber);
//斷言值是否不存在
testSubscriber.assertNever("D");
//斷言值是否相等
testSubscriber.assertValues("A", "B", "C");
//斷言值的數量是否相等
testSubscriber.assertValueCount(3);
//斷言是否結束
testSubscriber.assertTerminated();
}
2.from
@Test
public void testFrom() {
TestSubscriber<Integer> testSubscriber = new TestSubscriber<>();
//依次發射list中的數字
Flowable.fromIterable(Arrays.asList(1, 2)).subscribe(testSubscriber);
testSubscriber.assertValues(1, 2);
testSubscriber.assertValueCount(2);
testSubscriber.assertTerminated();
}
3.range
@Test
public void testRange() {
TestSubscriber<Integer> testSubscriber = new TestSubscriber<>();
//從3開始發射3個連續的int
Flowable.range(3, 3).subscribe(testSubscriber);
testSubscriber.assertValues(3, 4, 5);
testSubscriber.assertValueCount(3);
testSubscriber.assertTerminated();
}
4.repeat
@Test
public void testRepeat() {
TestSubscriber<Integer> testSubscriber = new TestSubscriber<>();
Flowable.fromIterable(Arrays.asList(1, 2))
.repeat(2) //重複發送2次
.subscribe(testSubscriber);
testSubscriber.assertValues(1, 2, 1, 2);
testSubscriber.assertValueCount(4);
testSubscriber.assertTerminated();
}
5.buffer
@Test
public void testBuffer() {
TestSubscriber<List<String>> testSubscriber = new TestSubscriber<>();
//緩衝2個發射一次
Flowable.just("A", "B", "C", "D")
.buffer(2)
.subscribe(testSubscriber);
testSubscriber.assertResult(Arrays.asList("A", "B"), Arrays.asList("C", "D"));
testSubscriber.assertValueCount(2);
testSubscriber.assertTerminated();
}
6.error
@Test
public void testError() {
TestSubscriber testSubscriber = new TestSubscriber();
Exception exception = new RuntimeException("error");
Flowable.error(exception).subscribe(testSubscriber);
//斷言錯誤是否一致
testSubscriber.assertError(exception);
//斷言錯誤信息是否一致
testSubscriber.assertErrorMessage("error");
}
7.interval
在測試有關時間的操作符時,可能我們的事件還沒有執行完,因此無法得到預期的輸出結果,斷言就無效了。當然我們可以使用上一篇說到的將異步轉爲同步思路,如下:
public class RxJavaRule implements TestRule {
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
RxJavaPlugins.setNewThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return Schedulers.trampoline();
}
});
try {
base.evaluate();
} finally {
RxJavaPlugins.reset();
}
}
};
}
}
測試代碼:
@Rule
public RxJavaRule rule = new RxJavaRule();
@Test
public void testInterval() {
TestSubscriber<Long> testSubscriber = new TestSubscriber<>();
//隔1秒發射一次,一共10次
Flowable.interval(1, TimeUnit.SECONDS)
.take(10)
.subscribe(testSubscriber);
testSubscriber.assertValueCount(10);
testSubscriber.assertTerminated();
}
但是這樣,我們的測試方法就要等待10s,也就是等待方法執行完畢。這完全都沒有了單元測試快捷的優點了。所以我來介紹一個更方便的方法。
RxJava 提供了 TestScheduler
,通過這個調度器可以實現對時間的操控。那麼我們的測試代碼就變成了:
@Test
public void testInterval() {
TestScheduler mTestScheduler = new TestScheduler();
TestSubscriber<Long> testSubscriber = new TestSubscriber<>();
//隔1秒發射一次,一共10次
Flowable.interval(1, TimeUnit.SECONDS, mTestScheduler)
.take(10)
.subscribe(testSubscriber);
//時間經過3秒
mTestScheduler.advanceTimeBy(3, TimeUnit.SECONDS);
testSubscriber.assertValues(0L, 1L, 2L);
testSubscriber.assertValueCount(3);
testSubscriber.assertNotTerminated();
//時間再經過2秒
mTestScheduler.advanceTimeBy(2, TimeUnit.SECONDS);
testSubscriber.assertValues(0L, 1L, 2L, 3L ,4L);
testSubscriber.assertValueCount(5);
testSubscriber.assertNotTerminated();
//時間到10秒
mTestScheduler.advanceTimeTo(10, TimeUnit.SECONDS);
testSubscriber.assertValueCount(10);
testSubscriber.assertTerminated();
}
是不是變得很方便,我們不僅不需要等待,還可以操控時間,可以到達任意的時間節點。
當然我們也可以利用@Rule將這個封裝一下,便於使用:
public class RxJavaTestSchedulerRule implements TestRule {
private final TestScheduler mTestScheduler = new TestScheduler();
public TestScheduler getTestScheduler() {
return mTestScheduler;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return mTestScheduler;
}
});
RxJavaPlugins.setNewThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return mTestScheduler;
}
});
RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return mTestScheduler;
}
});
RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
return mTestScheduler;
}
});
try {
base.evaluate();
} finally {
RxJavaPlugins.reset();
RxAndroidPlugins.reset();
}
}
};
}
}
8.timer
@Test
public void testTimer() {
TestScheduler mTestScheduler = new TestScheduler();
TestSubscriber<Long> testSubscriber = new TestSubscriber<>();
//延時5秒發射
Flowable.timer(5, TimeUnit.SECONDS, mTestScheduler)
.subscribe(testSubscriber);
//時間到5秒
mTestScheduler.advanceTimeTo(5, TimeUnit.SECONDS);
testSubscriber.assertValueCount(1);
testSubscriber.assertTerminated();
}
3.一個實戰小例子
我們用獲取驗證碼這個場景來檢驗今天的內容。點擊發送驗證碼按鈕,按鈕不能再點擊,倒計時120s,時間過了後,按鈕就可以點擊了。很簡單的一個場景。
界面:
代碼如下:
public class LoginActivity extends AppCompatActivity {
private Disposable mDisposable;
private TextView mTvSendIdentify;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mTvSendIdentify = (TextView) this.findViewById(R.id.tv_send_identify);
mTvSendIdentify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
getIdentify();
}
});
}
private void getIdentify() {
mTvSendIdentify.setEnabled(false);
// interval隔一秒發一次,到120結束
mDisposable = Observable
.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
.take(120)
.subscribeWith(new DisposableObserver<Long>() {
@Override
public void onComplete() {
mTvSendIdentify.setText(R.string.login_send_identify);
mTvSendIdentify.setEnabled(true);
}
@Override
public void onError(Throwable e) {}
@Override
public void onNext(Long aLong) {
mTvSendIdentify.setText(TextUtils.concat(String.valueOf(Math.abs(aLong - 120)), "秒後重試"));
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
if (mDisposable != null){
mDisposable.dispose();
}
}
}
測試代碼:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class LoginActivityTest {
private LoginActivity loginActivity;
private TextView mTvSendIdentify;
@Rule
public RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();
@Before
public void setUp(){
loginActivity = Robolectric.setupActivity(LoginActivity.class);
mTvSendIdentify = (TextView) loginActivity.findViewById(R.id.tv_send_identify);
}
@Test
public void testGetIdentify() throws Exception {
Application application = RuntimeEnvironment.application;
Assert.assertEquals(mTvSendIdentify.getText().toString(),
application.getString(R.string.login_send_identify));
// 觸發按鈕點擊
mTvSendIdentify.performClick();
// 時間到10秒
rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
assertEquals(mTvSendIdentify.isEnabled(), false);
assertEquals(mTvSendIdentify.getText().toString(), "111秒後重試");
// 時間到120秒
rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);
assertEquals(mTvSendIdentify.getText().toString(),
application.getString(R.string.login_send_identify));
assertEquals(mTvSendIdentify.isEnabled(), true);
}
}
如果你這樣做單元測試,是不是就不用傻傻的安裝後再點擊按鈕,等待着時間流逝了。
本篇所有代碼已上傳至Github。希望大家多多點贊支持!