異步無(wú)處不在践瓷,特別是網(wǎng)絡(luò)請(qǐng)求,必須在子線程中執(zhí)行爷辱。異步一般用來(lái)處理比較耗時(shí)的操作,除了網(wǎng)絡(luò)請(qǐng)求外還有數(shù)據(jù)庫(kù)操作朦肘、文件讀寫(xiě)等等饭弓。一個(gè)典型的異步方法如下:
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();
}
}
上面代碼里開(kāi)啟了一個(gè)異步線程,等待1秒之后在回調(diào)函數(shù)里成功返回?cái)?shù)據(jù)媒抠。通常情況下弟断,我們針對(duì)loadData()方法寫(xiě)如下單元測(cè)試:
@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());
}
執(zhí)行這段測(cè)試代碼,你會(huì)發(fā)現(xiàn)永遠(yuǎn)都不會(huì)通過(guò)趴生。因?yàn)?code>loadData()是一個(gè)異步方法阀趴,當(dāng)我們?cè)趫?zhí)行Assert.assertEquals()
方法時(shí),loadData()
異步方法里的代碼還沒(méi)執(zhí)行苍匆,所以list.size()
返回永遠(yuǎn)是0刘急。
這只是一個(gè)最簡(jiǎn)單的例子,我們代碼里肯定充斥著各種各樣的異步代碼浸踩,那么對(duì)于這些異步該怎么測(cè)試呢排霉?
要解決這個(gè)問(wèn)題,主要有2個(gè)思路:一是等待異步操作完成民轴,然后在進(jìn)行assert
斷言;二是將異步操作變成同步操作球订。
1. 等待異步完成:使用CountDownLatch
前面的例子后裸,等待異步完成實(shí)際上就是等待callback函數(shù)執(zhí)行完畢,使用CountDownLatch可以達(dá)到這個(gè)目標(biāo)冒滩,不熟悉該類(lèi)的可自行搜索學(xué)習(xí)微驶。修改原來(lái)的測(cè)試用例代碼如下:
@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方法執(zhí)行完畢侯,喚醒測(cè)試方法執(zhí)行線程
latch.countDown();
}
@Override
public void onFail() {
}
});
try {
//測(cè)試方法線程會(huì)在這里暫停, 直到loadData()方法執(zhí)行完畢, 才會(huì)被喚醒繼續(xù)執(zhí)行
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Assert.assertEquals(3, list.size());
}
CountDownLatch適用場(chǎng)景:
1.方法里有callback函數(shù)調(diào)用的異步方法,如前面所介紹的這個(gè)例子因苹。
2.RxJava實(shí)現(xiàn)的異步苟耻,RxJava里的subscribe方法實(shí)際上與callback類(lèi)似,所以同樣適用扶檐。
CountDownLatch同樣有它的局限性凶杖,就是必須能夠在測(cè)試代碼里調(diào)用countDown()
方法,這就要求被測(cè)的異步方法必須有類(lèi)似callback的調(diào)用款筑,也就是說(shuō)異步方法的調(diào)用結(jié)果必須是通過(guò)callback調(diào)用通知出去的智蝠,如果我們采用其他通知方式,例如EventBus奈梳、Broadcast將結(jié)果通知出去杈湾,CountDownLatch則不能實(shí)現(xiàn)這種異步方法的測(cè)試了。
實(shí)際上攘须,可以使用synchronized
的wait/notify
機(jī)制實(shí)現(xiàn)同樣的功能漆撞。我們將測(cè)試代碼稍微改改如下:
@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相比而言,語(yǔ)義更簡(jiǎn)單于宙,使用起來(lái)方便很多浮驳。
2. 將異步變成同步
下面介紹幾種不同的異步實(shí)現(xiàn)。
2.1 使用RxJava
RxJava現(xiàn)在已經(jīng)被廣泛運(yùn)用于Android開(kāi)發(fā)中了限煞,特別是結(jié)合了Rotrofit框架之后抹恳,簡(jiǎn)直是異步網(wǎng)絡(luò)請(qǐng)求的神器。RxJava發(fā)展到現(xiàn)在最新的版本是RxJava2署驻,相比RxJava1做了很多改進(jìn)奋献,這里我們直接采用RxJava2來(lái)講述,RxJava1與之類(lèi)似旺上。對(duì)于前面的異步請(qǐng)求瓶蚂,我們采用RxJava2來(lái)改造之后,代碼如下:
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都是通過(guò)subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
來(lái)實(shí)現(xiàn)異步的宣吱,這段代碼表示所有操作都在IO線程里執(zhí)行窃这,最后的結(jié)果是在主線程實(shí)現(xiàn)回調(diào)的。這里要將異步變成同步的關(guān)鍵是改變subscribeOn()的執(zhí)行線程征候,有2種方式可以實(shí)現(xiàn):
- 將subscribeOn()以及observeOn()的參數(shù)通過(guò)依賴(lài)注入的方式注入進(jìn)來(lái)杭攻,正常運(yùn)行時(shí)跑在IO線程中,測(cè)試時(shí)跑在測(cè)試方法運(yùn)行所在的線程中疤坝,這樣就實(shí)現(xiàn)了異步變同步兆解。
- 使用RxJava2提供的RxJavaPlugins工具類(lèi),讓
Schedulers.io()
返回當(dāng)前測(cè)試方法運(yùn)行所在的線程跑揉。
@Before
public void setup() {
RxJavaPlugins.reset();
//設(shè)置Schedulers.io()返回的線程
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
//返回當(dāng)前的工作線程锅睛,這樣測(cè)試方法與之都是運(yùn)行在同一個(gè)線程了埠巨,從而實(shí)現(xiàn)異步變同步。
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()實(shí)現(xiàn)異步的方式现拒,唯一的建議是趕緊去使用其他的異步框架吧辣垒。
2.3 使用Executor
如果我們使用Executor來(lái)實(shí)現(xiàn)異步,可以使用依賴(lài)注入的方式印蔬,在測(cè)試環(huán)境中將一個(gè)同步的Executor注入進(jìn)去勋桶。實(shí)現(xiàn)一個(gè)同步的Executor很簡(jiǎn)單。
Executor executor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
2.4 AsyncTask
現(xiàn)在已經(jīng)不推薦使用AsyncTask
了扛点,如果一定要使用哥遮,建議使用AsyncTask.executeOnExecutor(Executor exec, Params... params)
方法,然后通過(guò)依賴(lài)注入的方式陵究,在測(cè)試環(huán)境中將同步的Executor
注入進(jìn)去眠饮。
小結(jié)
本文主要介紹了針對(duì)異步代碼進(jìn)行單元測(cè)試的2種方法:一是等待異步完成,二是將異步變成同步铜邮。前者需要寫(xiě)很多侵入性代碼仪召,通過(guò)加鎖等機(jī)制來(lái)實(shí)現(xiàn),并且必須符合callback機(jī)制松蒜。其他還有很多實(shí)現(xiàn)異步的方式扔茅,例如IntentService、HandlerThread秸苗、Loader等召娜,綜合比較下來(lái),使用RxJava2來(lái)實(shí)現(xiàn)異步是一個(gè)不錯(cuò)的方案惊楼,它不僅功能強(qiáng)大玖瘸,并且在單元測(cè)試中能毫無(wú)侵入性的將異步變成同步,在這里強(qiáng)烈推薦檀咙!
系列文章:
Android單元測(cè)試(一):前言
Android單元測(cè)試(二):什么是單元測(cè)試
Android單元測(cè)試(三):測(cè)試難點(diǎn)及方案選擇
Android單元測(cè)試(四):JUnit介紹
Android單元測(cè)試(五):JUnit進(jìn)階
Android單元測(cè)試(六):Mockito學(xué)習(xí)
Android單元測(cè)試(七):Robolectric介紹
Android單元測(cè)試(八):怎樣測(cè)試異步代碼