異步無處不在,特别是網絡請求,必須在子線程中執行。異步一般用來處理比較耗時的操作,除了網絡請求外還有資料庫操作、檔案讀寫等等。一個典型的異步方法如下:
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單元測試(八):怎樣測試異步代碼