Android單元測試(六):RxJava測試

隨着響應式編程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。希望大家多多點贊支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章