前言
在之前的系列博客中,主要圍繞的是測試工具的介紹與使用。經(jīng)過幾個月的沉寂,在項目中摸索與實踐單元測試,曾經(jīng)踩坑無數(shù),自己從中受益匪淺,確實是一段成長的歷程!今天準備一些干貨,給感興趣的同學借鑒一下,主要是分享在項目實踐過程中的經(jīng)驗總結(jié)以及對Android單元測試的理解,將以兩篇博客的篇幅進行詳細介紹,歡迎大家關(guān)注!
Precondition
需要明確的是,單元測試分為兩部分,即UI測試和邏輯測試,其兩者的實現(xiàn)方式是有所不同的,效率也是不一樣的。現(xiàn)在的項目中,大都使用MVP設計框架,它通過面向接口編程的方式,借助于Presenter這個中間層從而實現(xiàn)View層和Model層的隔離,不僅方便項目維護擴展,因其把依賴于Android環(huán)境的View層和純Java的數(shù)據(jù)邏輯處理層分離,還方便我們進行單元測試。工欲善其事,必先利其器,在實踐之前,我們要用MVP設計框架對項目進行重構(gòu),只有建立在良好的架構(gòu)和明確的層次,單元測試實施起來才能事半功倍。
先說UI測試,對應于MVP設計框架中的View層,所寫的Case代碼位于src/androidTest/java/
。既然是Android的UI,就依賴于Android環(huán)境,那么我們針對這個的單元測試覆蓋也就需要運行在Android虛擬機和Android真機上,想必你也知道,每當我們Run一次都需要好幾分鐘的等待時間,期間經(jīng)過編譯成apk,并把apk安裝在Android環(huán)境上。這就是為什么我們要把項目分為UI測試和邏輯測試,因為耗時。對Android UI測試,想必你可能了解,Google官方推出了Espresso,使用起來很方便,會在以后的博客中展開來說。
而邏輯測試,對應于MVP設計框架的Presenter層和Model層,所寫的Case代碼位于src/test/java/
。指的是純Java代碼的單元覆蓋,比如說登錄時對賬戶密碼合法性的校驗邏輯,再比如說是數(shù)據(jù)的請求、存儲、封裝等處理邏輯,這部分的代碼往往不依賴于Android環(huán)境,可能會對Android Context上下文的依賴,相對UI來說要純粹一些。看過之前博客的同學可能會知道,強烈推薦使用測試框架PowerMockito+Robolectric。
(1)、PowerMockito不僅可以mock Public數(shù)據(jù)對象,還可以mock Private、Final、Static、Singleton等數(shù)據(jù)對象,通過Mock數(shù)據(jù)對象的方式可以幫助我們隔離外部依賴,讓我們只專注于目標代碼輸入輸出等調(diào)用邏輯的測試;
(2)、Robolectric通過實現(xiàn)一套能在JVM能運行的Android代碼,為我們提供Android Application和Context的支持,因為在Model層需要依賴于Android Context上下文,比如說對Android數(shù)據(jù)庫Sqlite操作和SharedPreference等數(shù)據(jù)存儲操作。
邏輯測試
今天的重點是分享如何進行邏輯代碼的單元覆蓋,終于說到正題了。
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'
細心的同學會發(fā)現(xiàn),此處robolectric用的是老版本3.0,并沒有用最新的版本3.3。前方高能,從github的反饋中看出,新版本有坑還不穩(wěn)定。如果項目中需要讀取配置信息(如HTTPS的證書、預置數(shù)據(jù)),就得使用assets文件。默認情況下,robolectric3.0版本無法讀取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;
}
};
}
}
在代碼末尾處,你會發(fā)現(xiàn)下面代碼:
public int getTargetSdkVersion() {
return 18;
}
一個非常重要的細節(jié),若是不重寫指定Android版本的話,就會報錯java.lang.UnsupportedOperationException: Robolectric does not support API level 1, sorry!
,然而在最新的robolectric版本沒有這個Exception。說點題外話,除了重寫getTargetSdkVersion
方法這種方式,還可以在AndroidManifest.xml配置文件中指定compileSdkVersion,雖然可以解決這個Exception,但是你不覺得這種方式侵入性有點大嗎,在Android Studio中配置sdk版本是在gradle文件中配置,所以不推薦這種方式。
BaseRoboTestCase
避免重復代碼,定義抽象類BaseRoboTestCase,只要繼承重寫就可以開始單元測試之旅,是不是很方便呀!
@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() {
}
}
上面代碼涉及的知識會有點多,在這里我們只關(guān)注重點,更加詳細的可以參考我之前寫的博客。
1、通過
@RunWith(CustomTestRunner.class)
方式注入上面說到的自定義Runner。
2、不知道你注意到了沒有,上面寫了一個空的test()
測試方法,方法名可以隨意定義,這是為啥呢?是因為在終端上運行./gradlew testDebugUnitTest --continue
指令批量來跑src/test/java/
目錄下所有的單元測試Case時,會拋出異常java.lang.Exception: No runnable methods
。
3、公司項目使用的是RxJava+Retrofit+OKHttp框架來處理網(wǎng)絡請求和異步操作的,在對RxJava相關(guān)的代碼進行單元測試時,線程切換是非常重要。RxJava官方考慮到單元測試,為我們提供了Hook的方式來保證線程切換,通過RxAndroidPlugins.getInstance().registerSchedulersHook()
方法可以將其他線程的處理統(tǒng)一切換到我們指定線程Schedulers.immediate()
來處理,即當前單元測試跑的這個線程,如此一來方便單元測試驗證。
寫好Presenter
MVP設計框架中,如何寫好Presenter層,是一個很有藝術(shù)的問題。想當初初學MVP時,還是會按照之前MVC的慣性思維,會把部分的數(shù)據(jù)邏輯(比如說數(shù)據(jù)對象空、越界、合法性等判斷)處理放在Activity中,這樣導致的結(jié)果是,如果想單元測試這部分邏輯代碼,就會顯得比較麻煩,必須得在Android測試環(huán)境下執(zhí)行。其實,一個好的Presenter層應該是,包含絕大部分的數(shù)據(jù)處理邏輯,而View層只執(zhí)行UI的更新工作(setText、setVisibile、setFocus等),如此一來就很方便我們進行單元覆蓋Pressenter所有邏輯分支。換句話說,Presenter層直接影響到純Java代碼的覆蓋率了,進而關(guān)系到bug率。
隔離外部依賴
一個很普遍的問題是,要測試的目標類會有很多外部依賴,這些依賴的類/對象/資源又會有別的依賴,從而形成一個大的依賴樹,要在單元測試的環(huán)境中完整地構(gòu)建這樣的依賴,是一件很困難的事情。而通過Mock的方式,對測試的類所依賴的其他類和對象,進行mock構(gòu)建假對象,并定義這些假對象上的行為,然后提供給被測試對象使用。被測試對象像使用真的對象一樣使用它們。用這種方式,我們可以把測試的目標限定于被測試對象本身,就如同在被測試對象周圍做了一個劃斷,形成了一個盡量小的被測試目標。
但Mock的前提是你的代碼可以進行外部依賴注入,可能我們在不知覺中,就會在類中構(gòu)造并定義私有變量,或者在用到的時候直接new,讓我們沒法方便進行依賴注入,諸如此類都不是正確的姿勢。如下:
所以在coding時,對于外部依賴,盡量要提供接口可以注入依賴,否則我們難以入手。可以通過構(gòu)造函數(shù)的方式傳入外部依賴,也可以通過set方法,要是項目使用Dagger2框架,可以通過依賴注解的方式解決。正確的姿勢如下:
測試普通方法
當我們要對一個方法進行測試時,該如何下手呢?
- 有明確的返回值,做單元測試時,只需調(diào)用這個函數(shù),驗證其返回值是否符合預期結(jié)果,這個很簡單。
- 對于無返回值的void方法,這個方法只改變其對象內(nèi)部的一些屬性或者狀態(tài),就驗證它所改變的屬性和狀態(tài),可以通過ArgumentCaptor方式來捕獲并驗證中間狀態(tài),也可以驗證是否執(zhí)行外部依賴的方法。
測試異步方法
深切體會到,測試異步方法,是整個單元測試的難點和重點,為什么這么說呢?問題很明顯,當測試方法跑完了的時候,被測的異步代碼可能還在執(zhí)行沒跑完,這就有問題了。再者就是實現(xiàn)異步操作的框架比較多樣。下面有這么一個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 {
// 模擬耗時操作
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()
方法里面新建了一個線程來異步加載results字符串列表。如果我們按正常的方式寫對應的測試:
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());
}
}
你會發(fā)現(xiàn)上面的測試方法loadAync()
永遠會fail,這是因為在執(zhí)行 assertEquals(1, result.size());
的時候,loadAync()
里面啟動的線程壓根還沒執(zhí)行完畢呢,因此,callback里面的 result.addAll(list);
也沒有得到執(zhí)行,所以result.size()
返回永遠是0。
前方高能,重點來了,要解決這個問題:如何使用正確的姿勢來測試異步代碼。通常有兩種思路,一是等異步代碼執(zhí)行完了再執(zhí)行assert斷言操作,二是將異步變成同步。接下來,具體講講用這兩種思路怎樣來測試我們的異步代碼:
等待異步代碼執(zhí)行完畢
在上面的例子中,我們要做的其實就是是等待Callback里面的代碼執(zhí)行完畢后再執(zhí)行Asset斷言操作。要達到這個目的,大致有兩種實現(xiàn)方式:
(1)、使用Thread.sleep
估計大家的第一反應可能和我一樣,會使用這種休眠的方式來等待異步代碼執(zhí)行,可能是最簡單的方式,這種方式需要設置sleep的時間,所以不可控,建議不適用這種方式。結(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);
// 此處有坑,如果不加這行代碼,就會出現(xiàn)Handler沒有執(zhí)行Runnable的問題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
(2)、使用CountDownLatch
有一個非常好用的神器,那就是CountDownLatch。CountDownLatch是一個類,它有兩對配套使用的方法,那就是countDown()和await()。await()方法會阻塞當前線程,直到countDown()被調(diào)用了一定的次數(shù),這個次數(shù)就是在創(chuàng)建這個CountDownLatch對象時,傳入的構(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);
// 此處有坑,如果不加這行代碼,就會出現(xiàn)Handler沒有執(zhí)行Runnable的問題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
使用CountDownLatch來做單元測試,有一個很大的限制,侵入性很高,那就是countDown()必須在測試代碼里面寫。換句話說,異步操作必需提供Callback,在Callback中執(zhí)行countDown()方法。如果被測的異步方法(如上面例子的loadAync())不是通過Callback的方式來通知結(jié)果,而是通過EventBus來通知外面方法異步運行的結(jié)果,那CountDownLatch是無法解決這個異步方法的單元測試問題的。
將異步變成同步
將異步操作變成同步,是解決異步代碼測試問題的一種比較直觀的思路。這種思路往往比較復雜,根據(jù)項目的實際情況來抉擇,大致的思想就是將異步操作轉(zhuǎn)換到自己事先準備好的同步線程池來執(zhí)行。
(1)、通過Executor或ExecutorService方式
如果你的代碼是通過Executor或ExecutorService來做異步的,那在測試中把異步變成同步的做法,跟在測試中使用mock對象的方法是一樣的,那就是使用依賴注入。在測試代碼里面,將同步的Executor注入進去。創(chuàng)建同步的Executor對象很簡單,以下就是一個同步的Executor:
Executor immediateExecutor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
(2)、通過New Thread()方式
如果你在代碼里面直接通過new Thread()的方式來做異步,這種方式比較簡單粗暴,估計你在coding時很爽。但是不幸的告訴你,這樣的代碼是沒有辦法變成同步的。那么要做單元測試的話,就需要換成Executor這種方式來做異步操作。還是結(jié)合上面的例子,我們來實踐一下,修改之后的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);
}
}
接著我們看一下修改之后的測試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();
}
});
// 此處有坑,如果不加這行代碼,就會出現(xiàn)Handler沒有執(zhí)行Runnable的問題
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertEquals(1, result.size());
}
}
不知你有沒有感覺到,使用Executor方式之后,不管是源代碼還是測試代碼看起來都很清爽!
(3)、使用AsyncTask
Android提供AsyncTask類,很方便我們進行異步操作,初學Android時,很喜歡這種方式。進行單元測試時,建議使用 AsyncTask.executeOnExecutor(),而不是直接使用AsyncTask.execute(),通過依賴注入的方式,在測試環(huán)境下將同步的Executor傳進去進去。
(4)、使用RxJava
這個是不得不提的一種方法,鑒于強大的線程切換功能,越來越多的人使用RxJava來做異步操作,RxJava代碼的單元測試也是經(jīng)常被問到的一個問題。不管你是否用到RxJava,反正我現(xiàn)在的項目就用到了。至于如何將異步操作切換到同步執(zhí)行,之前已經(jīng)詳細講到了,可以回到上面再看看。
如何Mock網(wǎng)絡數(shù)據(jù)
當我們要對Presenter或者測試UI,考慮到根據(jù)網(wǎng)絡返回的數(shù)據(jù)覆蓋所有的分支情況,對于一個賬號在某一時刻,后端只會返回一種數(shù)據(jù)結(jié)果,這樣就限制了做其他情況的單元驗證。所以這個時候就需要我們Mock數(shù)據(jù)來模擬。鑒于項目中使用OKHTTP框架,只要自定義一個Interceptor,在這里進行攔截并Mock你想要的數(shù)據(jù),相對來說這種方式比較友好。
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();
}
}
涉及到的其它類,不是本博客的重點,就不一一列舉了。如果項目中不是使用OKHTTP網(wǎng)絡框架,而是其他的網(wǎng)絡框架如Volley、android-async-http等,還沒來得及去探索,感興趣的同學自己可以深入探索一下。
具體例子
說了這么多,所謂實踐是檢驗真理的唯一標準!下面我們針對具體的例子來實踐一把,項目中的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;
}
// 只展示當前要加載的頁碼的數(shù)據(jù),其他的過濾掉
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()
方法是一個沒有返回值的公有方法,那么我們該如何下手?首先依賴入?yún)?code>OneClickOrderResult,根據(jù)result
狀態(tài)來執(zhí)行邏輯,其次依賴iView
對象。因此,在進行單元測試時,通過mock的方式可以解決這兩個數(shù)據(jù)對象的依賴關(guān)系,mock出OneClickOrderResult
和iView
后,其他的就迎刃而解了。分析代碼,可以分為6個單元測試Case,如上面的注釋,覆蓋了onUpdateOrders()
方法所有的分支。測試方法如下:
public class OneClickBasePresenterTest extends BaseModelTest {
@Captor
private ArgumentCaptor<ArrayList<OneClickOrder.OneClickOrderItem>> captorItems;
@Test
public void onUpdateOrdersCase1() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
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)用被測方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具,驗證refreshNewestOrders方法是否被調(diào)用
Mockito.verify(mockView).refreshNewestOrders(null);
}
@Test
public void onUpdateOrdersCase2() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
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)用被測方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具,驗證refreshMoreOrders方法是否被調(diào)用
Mockito.verify(mockView).refreshMoreOrders(null);
}
@Test
public void onUpdateOrdersCase3() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
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);
// 通過ArgumentCaptor來捕獲refreshNewestOrders方法被調(diào)用時的入?yún)? Mockito.verify(mockView).refreshNewestOrders(captorItems.capture());
// 通過Assert斷言判斷ArgumentCaptor捕獲的入?yún)⒑蚷tems數(shù)據(jù)是否相等
Assert.assertEquals(captorItems.getValue().size(), items.size());
}
@Test
public void onUpdateOrdersCase4() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
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)用被測方法
presenter.onUpdateOrders(orderResult);
// 通過ArgumentCaptor來捕獲refreshMoreOrders方法被調(diào)用時的入?yún)? Mockito.verify(mockView).refreshMoreOrders(captorItems.capture());
Assert.assertEquals(captorItems.getValue().size(), items.size());
}
@Test
public void onUpdateOrdersCase5() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
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;
// 通過mock方式隔離依賴,mockView.isFragmentVisible()返回true
Mockito.when(mockView.isFragmentVisible()).thenReturn(true);
// 調(diào)用被測方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具,驗證showInputCaptchaDialog方法是否被調(diào)用
Mockito.verify(mockView).showInputCaptchaDialog();
}
@Test
public void onUpdateOrdersCase6() throws Exception {
// mock出IView對象,通過mock隔離外部依賴
OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
// 創(chuàng)建目標類
OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);
OneClickOrderResult orderResult = new OneClickOrderResult();
orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
orderResult.code = 500;
// 調(diào)用被測方法
presenter.onUpdateOrders(orderResult);
// 借助Mockito工具,驗證refreshError方法是否被調(diào)用
Mockito.verify(mockView).refreshError(orderResult.code, presenter.isLoadMoreOrders());
}
}
最后
本博客主要圍繞的是Android單元測試中的邏輯測試,自己對單元測試的理解,并結(jié)合實際代碼講解。如有不當之處,歡迎指正!下一篇博客將圍繞Android單元測試的UI測試。最后,非常感謝您對本篇博客的關(guān)注!