天天看點

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則不能實作這種異步方法的測試了。

實際上,可以使用

synchronized

wait/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單元測試(八):怎樣測試異步代碼