前言
在之前的系列博客中平匈,主要圍繞的是測(cè)試工具的介紹與使用。經(jīng)過(guò)幾個(gè)月的沉寂,在項(xiàng)目中摸索與實(shí)踐單元測(cè)試增炭,曾經(jīng)踩坑無(wú)數(shù)忍燥,自己從中受益匪淺,確實(shí)是一段成長(zhǎng)的歷程隙姿!今天準(zhǔn)備一些干貨梅垄,給感興趣的同學(xué)借鑒一下,主要是分享在項(xiàng)目實(shí)踐過(guò)程中的經(jīng)驗(yàn)總結(jié)以及對(duì)Android單元測(cè)試的理解输玷,將以兩篇博客的篇幅進(jìn)行詳細(xì)介紹队丝,歡迎大家關(guān)注!
Precondition
需要明確的是欲鹏,單元測(cè)試分為兩部分机久,即UI測(cè)試和邏輯測(cè)試,其兩者的實(shí)現(xiàn)方式是有所不同的赔嚎,效率也是不一樣的”旄牵現(xiàn)在的項(xiàng)目中,大都使用MVP設(shè)計(jì)框架尤误,它通過(guò)面向接口編程的方式侠畔,借助于Presenter這個(gè)中間層從而實(shí)現(xiàn)View層和Model層的隔離,不僅方便項(xiàng)目維護(hù)擴(kuò)展损晤,因其把依賴于Android環(huán)境的View層和純Java的數(shù)據(jù)邏輯處理層分離软棺,還方便我們進(jìn)行單元測(cè)試。工欲善其事沉馆,必先利其器码党,在實(shí)踐之前德崭,我們要用MVP設(shè)計(jì)框架對(duì)項(xiàng)目進(jìn)行重構(gòu)斥黑,只有建立在良好的架構(gòu)和明確的層次,單元測(cè)試實(shí)施起來(lái)才能事半功倍眉厨。
先說(shuō)UI測(cè)試锌奴,對(duì)應(yīng)于MVP設(shè)計(jì)框架中的View層,所寫的Case代碼位于src/androidTest/java/
憾股。既然是Android的UI鹿蜀,就依賴于Android環(huán)境,那么我們針對(duì)這個(gè)的單元測(cè)試覆蓋也就需要運(yùn)行在Android虛擬機(jī)和Android真機(jī)上服球,想必你也知道茴恰,每當(dāng)我們Run一次都需要好幾分鐘的等待時(shí)間,期間經(jīng)過(guò)編譯成apk斩熊,并把a(bǔ)pk安裝在Android環(huán)境上往枣。這就是為什么我們要把項(xiàng)目分為UI測(cè)試和邏輯測(cè)試,因?yàn)楹臅r(shí)。對(duì)Android UI測(cè)試分冈,想必你可能了解圾另,Google官方推出了Espresso,使用起來(lái)很方便雕沉,會(huì)在以后的博客中展開(kāi)來(lái)說(shuō)集乔。
而邏輯測(cè)試,對(duì)應(yīng)于MVP設(shè)計(jì)框架的Presenter層和Model層,所寫的Case代碼位于src/test/java/
坡椒。指的是純Java代碼的單元覆蓋扰路,比如說(shuō)登錄時(shí)對(duì)賬戶密碼合法性的校驗(yàn)邏輯,再比如說(shuō)是數(shù)據(jù)的請(qǐng)求肠牲、存儲(chǔ)幼衰、封裝等處理邏輯,這部分的代碼往往不依賴于Android環(huán)境缀雳,可能會(huì)對(duì)Android Context上下文的依賴渡嚣,相對(duì)UI來(lái)說(shuō)要純粹一些》视。看過(guò)之前博客的同學(xué)可能會(huì)知道识椰,強(qiáng)烈推薦使用測(cè)試框架PowerMockito+Robolectric。
(1)深碱、PowerMockito不僅可以mock Public數(shù)據(jù)對(duì)象腹鹉,還可以mock Private、Final敷硅、Static功咒、Singleton等數(shù)據(jù)對(duì)象,通過(guò)Mock數(shù)據(jù)對(duì)象的方式可以幫助我們隔離外部依賴绞蹦,讓我們只專注于目標(biāo)代碼輸入輸出等調(diào)用邏輯的測(cè)試力奋;
(2)、Robolectric通過(guò)實(shí)現(xiàn)一套能在JVM能運(yùn)行的Android代碼幽七,為我們提供Android Application和Context的支持景殷,因?yàn)樵贛odel層需要依賴于Android Context上下文,比如說(shuō)對(duì)Android數(shù)據(jù)庫(kù)Sqlite操作和SharedPreference等數(shù)據(jù)存儲(chǔ)操作澡屡。
邏輯測(cè)試
今天的重點(diǎn)是分享如何進(jìn)行邏輯代碼的單元覆蓋猿挚,終于說(shuō)到正題了。
build.gradle配置:
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.0'
testCompile 'org.robolectric:robolectric:3.0'
testCompile 'org.powermock:powermock-module-junit4:1.6.5'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
testCompile 'org.powermock:powermock-api-mockito:1.6.5'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)驶鹉,此處robolectric用的是老版本3.0绩蜻,并沒(méi)有用最新的版本3.3。前方高能室埋,從github的反饋中看出办绝,新版本有坑還不穩(wěn)定踏兜。如果項(xiàng)目中需要讀取配置信息(如HTTPS的證書、預(yù)置數(shù)據(jù))八秃,就得使用assets文件碱妆。默認(rèn)情況下,robolectric3.0版本無(wú)法讀取asset文件昔驱,還得自定義RobolectricTestRunner
疹尾。
自定義Runner
public class CustomTestRunner extends RobolectricTestRunner {
private static final String APP_MODULE_NAME = "app";
/**
* Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
* and res directory by default. Use the {@link org.robolectric.annotation.Config} annotation to configure.
*
* @param testClass the test class to be run
* @throws org.junit.runners.model.InitializationError if junit says so
*/
public CustomTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
protected AndroidManifest getAppManifest(Config config) {
String userDir = System.getProperty("user.dir", "./");
File current = new File(userDir);
String prefix;
if (new File(current, APP_MODULE_NAME).exists()) {
System.out.println("Probably running on AndroidStudio");
prefix = "./" + APP_MODULE_NAME;
} else if (new File(current.getParentFile(), APP_MODULE_NAME).exists()) {
System.out.println("Probably running on Console");
prefix = "../" + APP_MODULE_NAME;
} else {
throw new IllegalStateException("Could not find app module, app module should be \"app\" directory in the project.");
}
System.setProperty("android.manifest", prefix + "/src/main/AndroidManifest.xml");
System.setProperty("android.resources", prefix + "/src/main/res");
System.setProperty("android.assets", prefix + "/src/main/assets");
return new AndroidManifest(Fs.fileFromPath(prefix + "/src/main/AndroidManifest.xml"), Fs.fileFromPath(prefix + "/src/main/res"), Fs.fileFromPath(prefix + "/src/main/assets")) {
@Override
public int getTargetSdkVersion() {
return 18;
}
};
}
}
在代碼末尾處,你會(huì)發(fā)現(xiàn)下面代碼:
public int getTargetSdkVersion() {
return 18;
}
一個(gè)非常重要的細(xì)節(jié)骤肛,若是不重寫指定Android版本的話纳本,就會(huì)報(bào)錯(cuò)java.lang.UnsupportedOperationException: Robolectric does not support API level 1, sorry!
,然而在最新的robolectric版本沒(méi)有這個(gè)Exception腋颠。說(shuō)點(diǎn)題外話繁成,除了重寫getTargetSdkVersion
方法這種方式,還可以在AndroidManifest.xml配置文件中指定compileSdkVersion淑玫,雖然可以解決這個(gè)Exception巾腕,但是你不覺(jué)得這種方式侵入性有點(diǎn)大嗎,在Android Studio中配置sdk版本是在gradle文件中配置絮蒿,所以不推薦這種方式尊搬。
BaseRoboTestCase
避免重復(fù)代碼,定義抽象類BaseRoboTestCase土涝,只要繼承重寫就可以開(kāi)始單元測(cè)試之旅佛寿,是不是很方便呀!
@Config( shadows = {ShadowLog.class})
@RunWith(CustomTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public abstract class BaseRoboTestCase {
@Rule
public PowerMockRule rule = new PowerMockRule();
private static boolean hasInitRxJava = false;
@Before
public void setUp() {
ShadowLog.stream = System.out;
System.out.println("setUp now");
Robolectric.getShadowApplication();
if (!hasInitRxJava) {
hasInitRxJava = true;
initRxJava();
}
MockitoAnnotations.initMocks(this);
}
public Application getApplication() {
return Robolectric.application;
}
public Context getContext() {
return getApplication();
}
private void initRxJava() {
RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getIOScheduler() {
return Schedulers.immediate();
}
});
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
@Test
public void test() {
}
}
上面代碼涉及的知識(shí)會(huì)有點(diǎn)多但壮,在這里我們只關(guān)注重點(diǎn)冀泻,更加詳細(xì)的可以參考我之前寫的博客。
1蜡饵、通過(guò)
@RunWith(CustomTestRunner.class)
方式注入上面說(shuō)到的自定義Runner弹渔。
2、不知道你注意到了沒(méi)有验残,上面寫了一個(gè)空的test()
測(cè)試方法捞附,方法名可以隨意定義巾乳,這是為啥呢您没?是因?yàn)樵诮K端上運(yùn)行./gradlew testDebugUnitTest --continue
指令批量來(lái)跑src/test/java/
目錄下所有的單元測(cè)試Case時(shí),會(huì)拋出異常java.lang.Exception: No runnable methods
胆绊。
3氨鹏、公司項(xiàng)目使用的是RxJava+Retrofit+OKHttp框架來(lái)處理網(wǎng)絡(luò)請(qǐng)求和異步操作的,在對(duì)RxJava相關(guān)的代碼進(jìn)行單元測(cè)試時(shí)压状,線程切換是非常重要仆抵。RxJava官方考慮到單元測(cè)試跟继,為我們提供了Hook的方式來(lái)保證線程切換,通過(guò)RxAndroidPlugins.getInstance().registerSchedulersHook()
方法可以將其他線程的處理統(tǒng)一切換到我們指定線程Schedulers.immediate()
來(lái)處理镣丑,即當(dāng)前單元測(cè)試跑的這個(gè)線程舔糖,如此一來(lái)方便單元測(cè)試驗(yàn)證。
寫好Presenter
MVP設(shè)計(jì)框架中莺匠,如何寫好Presenter層金吗,是一個(gè)很有藝術(shù)的問(wèn)題。想當(dāng)初初學(xué)MVP時(shí)趣竣,還是會(huì)按照之前MVC的慣性思維摇庙,會(huì)把部分的數(shù)據(jù)邏輯(比如說(shuō)數(shù)據(jù)對(duì)象空、越界遥缕、合法性等判斷)處理放在Activity中卫袒,這樣導(dǎo)致的結(jié)果是,如果想單元測(cè)試這部分邏輯代碼单匣,就會(huì)顯得比較麻煩夕凝,必須得在Android測(cè)試環(huán)境下執(zhí)行。其實(shí)户秤,一個(gè)好的Presenter層應(yīng)該是迹冤,包含絕大部分的數(shù)據(jù)處理邏輯,而View層只執(zhí)行UI的更新工作(setText虎忌、setVisibile泡徙、setFocus等),如此一來(lái)就很方便我們進(jìn)行單元覆蓋Pressenter所有邏輯分支膜蠢。換句話說(shuō)堪藐,Presenter層直接影響到純Java代碼的覆蓋率了,進(jìn)而關(guān)系到bug率挑围。
隔離外部依賴
一個(gè)很普遍的問(wèn)題是礁竞,要測(cè)試的目標(biāo)類會(huì)有很多外部依賴,這些依賴的類/對(duì)象/資源又會(huì)有別的依賴杉辙,從而形成一個(gè)大的依賴樹(shù)模捂,要在單元測(cè)試的環(huán)境中完整地構(gòu)建這樣的依賴,是一件很困難的事情蜘矢。而通過(guò)Mock的方式狂男,對(duì)測(cè)試的類所依賴的其他類和對(duì)象,進(jìn)行mock構(gòu)建假對(duì)象品腹,并定義這些假對(duì)象上的行為岖食,然后提供給被測(cè)試對(duì)象使用。被測(cè)試對(duì)象像使用真的對(duì)象一樣使用它們舞吭。用這種方式泡垃,我們可以把測(cè)試的目標(biāo)限定于被測(cè)試對(duì)象本身析珊,就如同在被測(cè)試對(duì)象周圍做了一個(gè)劃斷,形成了一個(gè)盡量小的被測(cè)試目標(biāo)蔑穴。
但Mock的前提是你的代碼可以進(jìn)行外部依賴注入忠寻,可能我們?cè)诓恢X(jué)中,就會(huì)在類中構(gòu)造并定義私有變量存和,或者在用到的時(shí)候直接new锡溯,讓我們沒(méi)法方便進(jìn)行依賴注入,諸如此類都不是正確的姿勢(shì)哑姚。如下:
所以在coding時(shí)祭饭,對(duì)于外部依賴,盡量要提供接口可以注入依賴叙量,否則我們難以入手倡蝙。可以通過(guò)構(gòu)造函數(shù)的方式傳入外部依賴绞佩,也可以通過(guò)set方法寺鸥,要是項(xiàng)目使用Dagger2框架,可以通過(guò)依賴注解的方式解決品山。正確的姿勢(shì)如下:
測(cè)試普通方法
當(dāng)我們要對(duì)一個(gè)方法進(jìn)行測(cè)試時(shí)胆建,該如何下手呢?
- 有明確的返回值肘交,做單元測(cè)試時(shí)笆载,只需調(diào)用這個(gè)函數(shù),驗(yàn)證其返回值是否符合預(yù)期結(jié)果涯呻,這個(gè)很簡(jiǎn)單凉驻。
- 對(duì)于無(wú)返回值的void方法,這個(gè)方法只改變其對(duì)象內(nèi)部的一些屬性或者狀態(tài)复罐,就驗(yàn)證它所改變的屬性和狀態(tài)涝登,可以通過(guò)ArgumentCaptor方式來(lái)捕獲并驗(yàn)證中間狀態(tài),也可以驗(yàn)證是否執(zhí)行外部依賴的方法效诅。
測(cè)試異步方法
深切體會(huì)到胀滚,測(cè)試異步方法,是整個(gè)單元測(cè)試的難點(diǎn)和重點(diǎn)乱投,為什么這么說(shuō)呢咽笼?問(wèn)題很明顯,當(dāng)測(cè)試方法跑完了的時(shí)候篡腌,被測(cè)的異步代碼可能還在執(zhí)行沒(méi)跑完褐荷,這就有問(wèn)題了勾效。再者就是實(shí)現(xiàn)異步操作的框架比較多樣嘹悼。下面有這么一個(gè)AyncModel類:
public class AyncModel {
private Handler mUiHandler = new Handler(Looper.getMainLooper());
public void loadAync(final Callback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 模擬耗時(shí)操作
Thread.sleep(1000);
final List<String> results = new ArrayList<>();
results.add("test String");
mUiHandler.post(new Runnable() {
@Override
public void run() {
callback.onSuccess(results);
}
});
} catch (final InterruptedException e) {
e.printStackTrace();
mUiHandler.post(new Runnable() {
@Override
public void run() {
callback.onFailure(500, e.getMessage());
}
});
}
}
}).start();
}
interface Callback {
void onSuccess(List<String> results);
void onFailure(int code, String msg);
}
}
在上面的例子中叛甫,AyncModel
類的loadAync()
方法里面新建了一個(gè)線程來(lái)異步加載results字符串列表。如果我們按正常的方式寫對(duì)應(yīng)的測(cè)試:
public class AyncModelTest extends BaseRoboTestCase {
@Test
public void loadAync() throws Exception {
AyncModel model = new AyncModel();
final List<String> result = new ArrayList<>();
model.loadAync(new AyncModel.Callback() {
@Override
public void onSuccess(List<String> list) {
result.addAll(list);
}
@Override
public void onFailure(int code, String msg) {
fail();
}
});
assertEquals(1, result.size());
}
}
你會(huì)發(fā)現(xiàn)上面的測(cè)試方法loadAync()
永遠(yuǎn)會(huì)fail杨伙,這是因?yàn)樵趫?zhí)行 assertEquals(1, result.size());
的時(shí)候其监,loadAync()
里面啟動(dòng)的線程壓根還沒(méi)執(zhí)行完畢呢,因此限匣,callback里面的 result.addAll(list);
也沒(méi)有得到執(zhí)行抖苦,所以result.size()
返回永遠(yuǎn)是0。
前方高能米死,重點(diǎn)來(lái)了锌历,要解決這個(gè)問(wèn)題:如何使用正確的姿勢(shì)來(lái)測(cè)試異步代碼。通常有兩種思路峦筒,一是等異步代碼執(zhí)行完了再執(zhí)行assert斷言操作究西,二是將異步變成同步。接下來(lái)物喷,具體講講用這兩種思路怎樣來(lái)測(cè)試我們的異步代碼:
等待異步代碼執(zhí)行完畢
在上面的例子中卤材,我們要做的其實(shí)就是是等待Callback里面的代碼執(zhí)行完畢后再執(zhí)行Asset斷言操作。要達(dá)到這個(gè)目的峦失,大致有兩種實(shí)現(xiàn)方式:
(1)扇丛、使用Thread.sleep
估計(jì)大家的第一反應(yīng)可能和我一樣,會(huì)使用這種休眠的方式來(lái)等待異步代碼執(zhí)行尉辑,可能是最簡(jiǎn)單的方式帆精,這種方式需要設(shè)置sleep的時(shí)間,所以不可控隧魄,建議不適用這種方式实幕。結(jié)合上面的例子,具體演示一下:
public class AyncModelTest extends BaseRoboTestCase {
@Test
public void loadAync() throws Exception {
AyncModel model = new AyncModel();
final List<String> result = new ArrayList<>();
model.loadAync(new AyncModel.Callback() {
@Override
public void onSuccess(List<String> list) {
result.addAll(list);
}
@Override
public void onFailure(int code, String msg) {
fail();
}
});
// 使用sleep方式等待異步執(zhí)行
Thread.sleep(4000);
// 此處有坑堤器,如果不加這行代碼昆庇,就會(huì)出現(xiàn)Handler沒(méi)有執(zhí)行Runnable的問(wèn)題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
(2)、使用CountDownLatch
有一個(gè)非常好用的神器闸溃,那就是CountDownLatch整吆。CountDownLatch是一個(gè)類,它有兩對(duì)配套使用的方法辉川,那就是countDown()和await()表蝙。await()方法會(huì)阻塞當(dāng)前線程,直到countDown()被調(diào)用了一定的次數(shù)乓旗,這個(gè)次數(shù)就是在創(chuàng)建這個(gè)CountDownLatch對(duì)象時(shí)府蛇,傳入的構(gòu)造參數(shù)。結(jié)合上面的例子屿愚,具體如下:
public class AyncModelTest extends BaseRoboTestCase {
@Test
public void loadAync() throws Exception {
// 使用CountDownLatch
final CountDownLatch latch = new CountDownLatch(1);
AyncModel model = new AyncModel();
final List<String> result = new ArrayList<>();
model.loadAync(new AyncModel.Callback() {
@Override
public void onSuccess(List<String> list) {
result.addAll(list);
latch.countDown();
}
@Override
public void onFailure(int code, String msg) {
fail();
latch.countDown();
}
});
latch.await(3, TimeUnit.SECONDS);
// 此處有坑汇跨,如果不加這行代碼务荆,就會(huì)出現(xiàn)Handler沒(méi)有執(zhí)行Runnable的問(wèn)題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
使用CountDownLatch來(lái)做單元測(cè)試,有一個(gè)很大的限制穷遂,侵入性很高函匕,那就是countDown()必須在測(cè)試代碼里面寫。換句話說(shuō)蚪黑,異步操作必需提供Callback盅惜,在Callback中執(zhí)行countDown()方法。如果被測(cè)的異步方法(如上面例子的loadAync())不是通過(guò)Callback的方式來(lái)通知結(jié)果忌穿,而是通過(guò)EventBus來(lái)通知外面方法異步運(yùn)行的結(jié)果抒寂,那CountDownLatch是無(wú)法解決這個(gè)異步方法的單元測(cè)試問(wèn)題的。
將異步變成同步
將異步操作變成同步掠剑,是解決異步代碼測(cè)試問(wèn)題的一種比較直觀的思路蓬推。這種思路往往比較復(fù)雜,根據(jù)項(xiàng)目的實(shí)際情況來(lái)抉擇澡腾,大致的思想就是將異步操作轉(zhuǎn)換到自己事先準(zhǔn)備好的同步線程池來(lái)執(zhí)行沸伏。
(1)、通過(guò)Executor或ExecutorService方式
如果你的代碼是通過(guò)Executor或ExecutorService來(lái)做異步的动分,那在測(cè)試中把異步變成同步的做法毅糟,跟在測(cè)試中使用mock對(duì)象的方法是一樣的,那就是使用依賴注入澜公。在測(cè)試代碼里面姆另,將同步的Executor注入進(jìn)去。創(chuàng)建同步的Executor對(duì)象很簡(jiǎn)單坟乾,以下就是一個(gè)同步的Executor:
Executor immediateExecutor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
(2)迹辐、通過(guò)New Thread()方式
如果你在代碼里面直接通過(guò)new Thread()的方式來(lái)做異步,這種方式比較簡(jiǎn)單粗暴甚侣,估計(jì)你在coding時(shí)很爽明吩。但是不幸的告訴你,這樣的代碼是沒(méi)有辦法變成同步的殷费。那么要做單元測(cè)試的話印荔,就需要換成Executor這種方式來(lái)做異步操作。還是結(jié)合上面的例子详羡,我們來(lái)實(shí)踐一下仍律,修改之后的AyncModel類如下:
public class AyncModel {
private Handler mUiHandler = new Handler(Looper.getMainLooper());
private Executor executor;
public AyncModel(Executor executor) {
this.executor = executor;
}
public void loadAync(final Callback callback) {
if (executor == null) {
executor = Executors.newCachedThreadPool();
}
executor.execute(new Runnable() {
@Override
public void run() {
final List<String> repos = new ArrayList<>();
repos.add("test String");
mUiHandler.post(new Runnable() {
@Override
public void run() {
callback.onSuccess(repos);
}
});
}
});
}
interface Callback {
void onSuccess(List<String> results);
void onFailure(int code, String msg);
}
}
接著我們看一下修改之后的測(cè)試Case:
public class AyncModelTest extends BaseRoboTestCase {
@Test
public void loadAync() throws Exception {
// Executor
Executor immediateExecutor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
AyncModel model = new AyncModel(immediateExecutor);
final List<String> result = new ArrayList<>();
model.loadAync(new AyncModel.Callback() {
@Override
public void onSuccess(List<String> list) {
result.addAll(list);
}
@Override
public void onFailure(int code, String msg) {
fail();
}
});
// 此處有坑,如果不加這行代碼实柠,就會(huì)出現(xiàn)Handler沒(méi)有執(zhí)行Runnable的問(wèn)題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
不知你有沒(méi)有感覺(jué)到水泉,使用Executor方式之后,不管是源代碼還是測(cè)試代碼看起來(lái)都很清爽!
(3)草则、使用AsyncTask
Android提供AsyncTask類钢拧,很方便我們進(jìn)行異步操作,初學(xué)Android時(shí)畔师,很喜歡這種方式娶靡。進(jìn)行單元測(cè)試時(shí)牧牢,建議使用 AsyncTask.executeOnExecutor()看锉,而不是直接使用AsyncTask.execute(),通過(guò)依賴注入的方式塔鳍,在測(cè)試環(huán)境下將同步的Executor傳進(jìn)去進(jìn)去伯铣。
(4)、使用RxJava
這個(gè)是不得不提的一種方法轮纫,鑒于強(qiáng)大的線程切換功能腔寡,越來(lái)越多的人使用RxJava來(lái)做異步操作,RxJava代碼的單元測(cè)試也是經(jīng)常被問(wèn)到的一個(gè)問(wèn)題掌唾。不管你是否用到RxJava放前,反正我現(xiàn)在的項(xiàng)目就用到了。至于如何將異步操作切換到同步執(zhí)行糯彬,之前已經(jīng)詳細(xì)講到了凭语,可以回到上面再看看。
如何Mock網(wǎng)絡(luò)數(shù)據(jù)
當(dāng)我們要對(duì)Presenter或者測(cè)試UI撩扒,考慮到根據(jù)網(wǎng)絡(luò)返回的數(shù)據(jù)覆蓋所有的分支情況似扔,對(duì)于一個(gè)賬號(hào)在某一時(shí)刻,后端只會(huì)返回一種數(shù)據(jù)結(jié)果搓谆,這樣就限制了做其他情況的單元驗(yàn)證炒辉。所以這個(gè)時(shí)候就需要我們Mock數(shù)據(jù)來(lái)模擬。鑒于項(xiàng)目中使用OKHTTP框架泉手,只要自定義一個(gè)Interceptor黔寇,在這里進(jìn)行攔截并Mock你想要的數(shù)據(jù),相對(duì)來(lái)說(shuō)這種方式比較友好斩萌。
OkHttpMockInterceptor
類如下:
public class OkHttpMockInterceptor implements Interceptor {
public OkHttpMockInterceptor() {
}
@Override
public Response intercept(Chain chain) throws IOException {
Response response = null;
HttpUrl url = chain.request().url();
String sym = "";
String query = url.encodedQuery() == null ? "" : url.encodedQuery();
if (!query.equals("")) {
sym = "?";
}
String assetPath = url.encodedPath() + sym + query;
if (JsonStringHelper.isPathExist(assetPath)) {
response = mock(chain, assetPath);
}
if (response == null) {
response = chain.proceed(chain.request());
}
return response;
}
private Response mock(Chain chain, String assetPath) {
if (assetPath == null || "".equals(assetPath)) {
return null;
}
String jsonResult = JsonStringHelper.getMockJsonString(assetPath);
HttpResponse httpResponse = (HttpResponse) GsonHelper.fromJson(jsonResult, HttpResponse.class);
return new Response.Builder()
.code(Integer.valueOf(httpResponse.code))
.message(httpResponse.msg)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), jsonResult))
.addHeader("content-type", "application/json")
.build();
}
}
涉及到的其它類啡氢,不是本博客的重點(diǎn),就不一一列舉了术裸。如果項(xiàng)目中不是使用OKHTTP網(wǎng)絡(luò)框架倘是,而是其他的網(wǎng)絡(luò)框架如Volley、android-async-http等袭艺,還沒(méi)來(lái)得及去探索搀崭,感興趣的同學(xué)自己可以深入探索一下。
具體例子
說(shuō)了這么多,所謂實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)瘤睹!下面我們針對(duì)具體的例子來(lái)實(shí)踐一把升敲,項(xiàng)目中的onUpdateOrders(OneClickOrderResult result)
方法如下:
public void onUpdateOrders(OneClickOrderResult result) {
if (result == null || result.orderSource == null || !result.orderSource.equals(orderSource)) {
return;
}
handleUpdateOrders(result);
if (result.code == 200) {
if (result.data == null) {
if (curPage == ORDER_PAGE_INIT) {
// case1
iView.refreshNewestOrders(null);
} else if (curPage > ORDER_PAGE_INIT) {
// case2
iView.refreshMoreOrders(null);
}
return;
}
// 只展示當(dāng)前要加載的頁(yè)碼的數(shù)據(jù),其他的過(guò)濾掉
if (curPage != result.data.getCurrPage() && result.data.getCurrPage() > 0) {
return;
}
pageCount = result.data.getPageCount();
if (curPage == ORDER_PAGE_INIT) {
// case3
iView.refreshNewestOrders(filterHistoryOrders(result.data.getOrderList()));
} else if (curPage > ORDER_PAGE_INIT) {
// case4
iView.refreshMoreOrders(filterHistoryOrders(result.data.getOrderList()));
}
return;
}
if (result.code == OneClickFragment.ERROR_ID_MEITUAN_VISIT_OUT_OF_LIMIT && iView.isFragmentVisible()) {
// case5
iView.showInputCaptchaDialog();
return;
}
// case6
iView.refreshError(result.code, curPage > ORDER_PAGE_INIT);
}
onUpdateOrders()
方法是一個(gè)沒(méi)有返回值的公有方法轰传,那么我們?cè)撊绾蜗率致康常渴紫纫蕾嚾雲(yún)?code>OneClickOrderResult,根據(jù)result
狀態(tài)來(lái)執(zhí)行邏輯获茬,其次依賴iView
對(duì)象港庄。因此,在進(jìn)行單元測(cè)試時(shí)恕曲,通過(guò)mock的方式可以解決這兩個(gè)數(shù)據(jù)對(duì)象的依賴關(guān)系鹏氧,mock出OneClickOrderResult
和iView
后,其他的就迎刃而解了佩谣。分析代碼把还,可以分為6個(gè)單元測(cè)試Case,如上面的注釋茸俭,覆蓋了onUpdateOrders()
方法所有的分支吊履。測(cè)試方法如下:
public class OneClickBasePresenterTest extends BaseModelTest {
@Captor
private ArgumentCaptor<ArrayList<OneClickOrder.OneClickOrderItem>> captorItems;
@Test
public void onUpdateOrdersCase1() throws Exception {
// mock出IView對(duì)象,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
// 根據(jù)Case自己創(chuàng)建數(shù)據(jù)依賴
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 200;
orderResult.data = null;
presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT;
// 調(diào)用被測(cè)方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具调鬓,驗(yàn)證refreshNewestOrders方法是否被調(diào)用
Mockito.verify(mockView).refreshNewestOrders(null);
}
@Test
public void onUpdateOrdersCase2() throws Exception {
// mock出IView對(duì)象艇炎,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 200;
orderResult.data = null;
presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT + 1;
// 調(diào)用被測(cè)方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具,驗(yàn)證refreshMoreOrders方法是否被調(diào)用
Mockito.verify(mockView).refreshMoreOrders(null);
}
@Test
public void onUpdateOrdersCase3() throws Exception {
// mock出IView對(duì)象袖迎,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
// mock數(shù)據(jù)依賴OneClickOrderResult
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT;
orderResult.code = 200;
// mock數(shù)據(jù)依賴OneClickOrder
OneClickOrder data = Mockito.mock(OneClickOrder.class);
Mockito.when(data.getCurrPage()).thenReturn(presenter.curPage);
Parcel in = Mockito.mock(Parcel.class);
Mockito.when(in.readString()).thenReturn("1001");
Mockito.when(in.readInt()).thenReturn(1001);
OneClickOrder.OneClickOrderItem item = new OneClickOrder.OneClickOrderItem(in);
ArrayList<OneClickOrder.OneClickOrderItem> items = new ArrayList<>();
items.add(item);
items.add(item);
Mockito.when(data.getOrderList()).thenReturn(items);
orderResult.data = data;
presenter.onUpdateOrders(orderResult);
// 通過(guò)ArgumentCaptor來(lái)捕獲refreshNewestOrders方法被調(diào)用時(shí)的入?yún)? Mockito.verify(mockView).refreshNewestOrders(captorItems.capture());
// 通過(guò)Assert斷言判斷ArgumentCaptor捕獲的入?yún)⒑蚷tems數(shù)據(jù)是否相等
Assert.assertEquals(captorItems.getValue().size(), items.size());
}
@Test
public void onUpdateOrdersCase4() throws Exception {
// mock出IView對(duì)象冕臭,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
// mock數(shù)據(jù)依賴OneClickOrderResult
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 200;
presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT + 1;
// mock數(shù)據(jù)依賴OneClickOrder
OneClickOrder data = Mockito.mock(OneClickOrder.class);
Mockito.when(data.getCurrPage()).thenReturn(presenter.curPage);
Parcel in = Mockito.mock(Parcel.class);
Mockito.when(in.readString()).thenReturn("1001");
Mockito.when(in.readInt()).thenReturn(1001);
OneClickOrder.OneClickOrderItem item = new OneClickOrder.OneClickOrderItem(in);
ArrayList<OneClickOrder.OneClickOrderItem> items = new ArrayList<>();
items.add(item);
items.add(item);
Mockito.when(data.getOrderList()).thenReturn(items);
orderResult.data = data;
// 調(diào)用被測(cè)方法
presenter.onUpdateOrders(orderResult);
// 通過(guò)ArgumentCaptor來(lái)捕獲refreshMoreOrders方法被調(diào)用時(shí)的入?yún)? Mockito.verify(mockView).refreshMoreOrders(captorItems.capture());
Assert.assertEquals(captorItems.getValue().size(), items.size());
}
@Test
public void onUpdateOrdersCase5() throws Exception {
// mock出IView對(duì)象,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 200;
orderResult.code = OneClickFragment.ERROR_ID_MEITUAN_VISIT_OUT_OF_LIMIT;
// 通過(guò)mock方式隔離依賴燕锥,mockView.isFragmentVisible()返回true
Mockito.when(mockView.isFragmentVisible()).thenReturn(true);
// 調(diào)用被測(cè)方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具辜贵,驗(yàn)證showInputCaptchaDialog方法是否被調(diào)用
Mockito.verify(mockView).showInputCaptchaDialog();
}
@Test
public void onUpdateOrdersCase6() throws Exception {
// mock出IView對(duì)象,通過(guò)mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標(biāo)類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 500;
// 調(diào)用被測(cè)方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具归形,驗(yàn)證refreshError方法是否被調(diào)用
Mockito.verify(mockView).refreshError(orderResult.code, presenter.isLoadMoreOrders());
}
}
最后
本博客主要圍繞的是Android單元測(cè)試中的邏輯測(cè)試托慨,自己對(duì)單元測(cè)試的理解,并結(jié)合實(shí)際代碼講解暇榴。如有不當(dāng)之處厚棵,歡迎指正!下一篇博客將圍繞Android單元測(cè)試的UI測(cè)試蔼紧。最后婆硬,非常感謝您對(duì)本篇博客的關(guān)注!