Android單元測試(八):怎樣測試異步代碼

異步無處不在,特別是網絡請求,必須在子線程中執行。異步一般用來處理比較耗時的操作,除了網絡請求外還有數據庫操作、文件讀寫等等。一個典型的異步方法如下:

public class DataManager {

    public interface OnDataListener {

        public void onSuccess(List<String> dataList);

        public void onFail();
    }

    public void loadData(final OnDataListener listener) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);

                    List<String> dataList = new ArrayList<String>();
                    dataList.add("11");
                    dataList.add("22");
                    dataList.add("33");

                    if(listener != null) {
                        listener.onSuccess(dataList);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    if(listener != null) {
                        listener.onFail();
                    }
                }
            }
        }).start();
    }
}

上面代碼里開啟了一個異步線程,等待1秒之后在回調函數里成功返回數據。通常情況下,我們針對loadData()方法寫如下單元測試:

    @Test
    public void testGetData() {
        final List<String> list = new ArrayList<String>();
        DataManager dataManager = new DataManager();
        dataManager.loadData(new DataManager.OnDataListener() {
            @Override
            public void onSuccess(List<String> dataList) {
                if(dataList != null) {
                    list.addAll(dataList);
                }
            }

            @Override
            public void onFail() {
            }
        });
        Assert.assertEquals(3, list.size());
    }

執行這段測試代碼,你會發現永遠都不會通過。因為loadData()是一個異步方法,當我們在執行Assert.assertEquals()方法時,loadData()異步方法里的代碼還沒執行,所以list.size()返回永遠是0。
這只是一個最簡單的例子,我們代碼里肯定充斥著各種各樣的異步代碼,那么對于這些異步該怎么測試呢?

要解決這個問題,主要有2個思路:一是等待異步操作完成,然后在進行assert斷言;二是將異步操作變成同步操作。

1. 等待異步完成:使用CountDownLatch

前面的例子,等待異步完成實際上就是等待callback函數執行完畢,使用CountDownLatch可以達到這個目標,不熟悉該類的可自行搜索學習。修改原來的測試用例代碼如下:

    @Test
    public void testGetData() {
        final List<String> list = new ArrayList<String>();
        DataManager dataManager = new DataManager();
        final CountDownLatch latch = new CountDownLatch(1);
        dataManager.loadData(new DataManager.OnDataListener() {
            @Override
            public void onSuccess(List<String> dataList) {
                if(dataList != null) {
                    list.addAll(dataList);
                }
                //callback方法執行完畢侯,喚醒測試方法執行線程
                latch.countDown();
            }

            @Override
            public void onFail() {
            }
        });
        try {
            //測試方法線程會在這里暫停, 直到loadData()方法執行完畢, 才會被喚醒繼續執行
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Assert.assertEquals(3, list.size());
    }

CountDownLatch適用場景:
1.方法里有callback函數調用的異步方法,如前面所介紹的這個例子。
2.RxJava實現的異步,RxJava里的subscribe方法實際上與callback類似,所以同樣適用。

CountDownLatch同樣有它的局限性,就是必須能夠在測試代碼里調用countDown()方法,這就要求被測的異步方法必須有類似callback的調用,也就是說異步方法的調用結果必須是通過callback調用通知出去的,如果我們采用其他通知方式,例如EventBus、Broadcast將結果通知出去,CountDownLatch則不能實現這種異步方法的測試了。

實際上,可以使用synchronizedwait/notify機制實現同樣的功能。我們將測試代碼稍微改改如下:

    @Test
    public void testGetData() {
        final List<String> list = new ArrayList<String>();
        DataManager dataManager = new DataManager();
        final Object lock = new Object();
        dataManager.loadData(new DataManager.OnDataListener() {
            @Override
            public void onSuccess(List<String> dataList) {
                if(dataList != null) {
                    list.addAll(dataList);
                }
                synchronized (lock) {
                    lock.notify();
                }
            }

            @Override
            public void onFail() {
            }
        });
        try {
            synchronized (lock) {
                lock.wait();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Assert.assertEquals(3, list.size());
    }

CountDownLatch與wait/notify相比而言,語義更簡單,使用起來方便很多。

2. 將異步變成同步

下面介紹幾種不同的異步實現。

2.1 使用RxJava

RxJava現在已經被廣泛運用于Android開發中了,特別是結合了Rotrofit框架之后,簡直是異步網絡請求的神器。RxJava發展到現在最新的版本是RxJava2,相比RxJava1做了很多改進,這里我們直接采用RxJava2來講述,RxJava1與之類似。對于前面的異步請求,我們采用RxJava2來改造之后,代碼如下:

    public Observable<List<String>> loadData() {
        return Observable.create(new ObservableOnSubscribe<List<String>>() {
            @Override
            public void subscribe(ObservableEmitter<List<String>> e) throws Exception {
                Thread.sleep(1000);
                List<String> dataList = new ArrayList<String>();
                dataList.add("11");
                dataList.add("22");
                dataList.add("33");
                e.onNext(dataList);
                e.onComplete();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

RxJava2都是通過subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())來實現異步的,這段代碼表示所有操作都在IO線程里執行,最后的結果是在主線程實現回調的。這里要將異步變成同步的關鍵是改變subscribeOn()的執行線程,有2種方式可以實現:

  • 將subscribeOn()以及observeOn()的參數通過依賴注入的方式注入進來,正常運行時跑在IO線程中,測試時跑在測試方法運行所在的線程中,這樣就實現了異步變同步。
  • 使用RxJava2提供的RxJavaPlugins工具類,讓Schedulers.io()返回當前測試方法運行所在的線程。
    @Before
    public void setup() {
        RxJavaPlugins.reset();
        //設置Schedulers.io()返回的線程
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                //返回當前的工作線程,這樣測試方法與之都是運行在同一個線程了,從而實現異步變同步。
                return Schedulers.trampoline();
            }
        });
    }

    @Test
    public void testGetDataAsync() {    
        final List<String> list = new ArrayList<String>();
        DataManager dataManager = new DataManager();
        dataManager.loadData().subscribe(new Consumer<List<String>>() {
            @Override
            public void accept(List<String> dataList) throws Exception {
                if(dataList != null) {
                    list.addAll(dataList);
                }
            }
        }, new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {

            }
        });
        Assert.assertEquals(3, list.size());
    }
2.2 new Thread()方式做異步操作

如果你的代碼里還有直接new Thread()實現異步的方式,唯一的建議是趕緊去使用其他的異步框架吧。

2.3 使用Executor

如果我們使用Executor來實現異步,可以使用依賴注入的方式,在測試環境中將一個同步的Executor注入進去。實現一個同步的Executor很簡單。

    Executor executor = new Executor() {
        @Override
        public void execute(Runnable command) {
            command.run();
        }
    };
2.4 AsyncTask

現在已經不推薦使用AsyncTask了,如果一定要使用,建議使用AsyncTask.executeOnExecutor(Executor exec, Params... params)方法,然后通過依賴注入的方式,在測試環境中將同步的Executor注入進去。

小結

本文主要介紹了針對異步代碼進行單元測試的2種方法:一是等待異步完成,二是將異步變成同步。前者需要寫很多侵入性代碼,通過加鎖等機制來實現,并且必須符合callback機制。其他還有很多實現異步的方式,例如IntentService、HandlerThread、Loader等,綜合比較下來,使用RxJava2來實現異步是一個不錯的方案,它不僅功能強大,并且在單元測試中能毫無侵入性的將異步變成同步,在這里強烈推薦!

系列文章:
Android單元測試(一):前言
Android單元測試(二):什么是單元測試
Android單元測試(三):測試難點及方案選擇
Android單元測試(四):JUnit介紹
Android單元測試(五):JUnit進階
Android單元測試(六):Mockito學習
Android單元測試(七):Robolectric介紹
Android單元測試(八):怎樣測試異步代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。