Android單元測試—邏輯測試

前言

在之前的系列博客中,主要圍繞的是測試工具的介紹與使用。經(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)和明確的層次,單元測試實施起來才能事半功倍。

MVP

先說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框架,可以通過依賴注解的方式解決。正確的姿勢如下:

外部依賴正確的使用姿勢

測試普通方法

當我們要對一個方法進行測試時,該如何下手呢?

  1. 有明確的返回值,做單元測試時,只需調(diào)用這個函數(shù),驗證其返回值是否符合預期結(jié)果,這個很簡單。
  2. 對于無返回值的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。

Test Aync Fail

前方高能,重點來了,要解決這個問題:如何使用正確的姿勢來測試異步代碼。通常有兩種思路,一是等異步代碼執(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出OneClickOrderResultiView后,其他的就迎刃而解了。分析代碼,可以分為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)注!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容