Android Junit 單元測(cè)試、異步測(cè)試方法簡(jiǎn)介及異步測(cè)試框架指南

Android Junit 單元測(cè)試、異步測(cè)試方法簡(jiǎn)介及異步測(cè)試框架指南

本文解決的問題

1. 如何使用junit 做Android 單元測(cè)試

2. 如何使用junit 做Android 異步接口單元測(cè)試

3. 使用作者封裝的框架,優(yōu)雅地用junit 做Android 異步接口單元測(cè)試 [doge]

Junit 作為Android Studio 原生支持的測(cè)試框架可以很方便的執(zhí)行單元測(cè)試,并且通過注解 @Test 可以直接標(biāo)記方法為測(cè)試case 然后在子線程中執(zhí)行。 標(biāo)記為@UiThreadTest 時(shí),測(cè)試case 將在 ui線程中執(zhí)行。

但是由于junit 本身的設(shè)計(jì),當(dāng)每個(gè)test方法執(zhí)行結(jié)束時(shí),該方法的運(yùn)行線程會(huì)一并kill掉, 因此對(duì)于異步調(diào)用的方法,子線程會(huì)一并回收,回調(diào)函數(shù)也無法執(zhí)行。

舉個(gè)栗子,以下的測(cè)試case 將無法收到回調(diào)并會(huì)報(bào)錯(cuò)


@Runwith(Junit4.calss)

class Test1{

    public static final String TAG="sample test";

    @Test

    public void test1(){

        new YourAsyncJob().run(new YourAsyncTestCallback(){

            @Override

            public void onFinished(){

                Log.i(TAG, "async call back");

            }

        });

        Log.i(TAG, "run async ok");

    }

}

解決方法 阻塞 test case

既然測(cè)試線程死掉之后對(duì)應(yīng)子任務(wù)都會(huì)失敗,最直接的方案就是直接阻塞對(duì)應(yīng)

<span id="section1">1 線程鎖</span>

慶幸Java 提供了極其好用的原生api。CountDownLatch 能夠直接阻塞線程,等待完成。 當(dāng)調(diào)用 await()方法時(shí),對(duì)應(yīng)線程會(huì)阻塞至 countdownlatch 的 count 變?yōu)? 時(shí),恢復(fù)運(yùn)行。因此我們得到以下方案


@Runwith(Junit4.calss)

class Test1{

    public static final String TAG="sample test";

    @Test

    public void test1(){

        final CountDownLatch mutex = new CountDownLatch(1);

        new YourAsyncJob().run(new YourAsyncTestCallback(){

            @Override

            public void onFinished(){

                Log.i(TAG, "async call back");

                mutex.countDown();

            }

        });

        Log.i(TAG, "run async ok");

        mutex.await();

    }

}

跑了一下,似乎可行,log 出來了。

然而在實(shí)際使用中又遇見了新的問題。

2 Looper 阻塞(Handler thread)

做過sdk的同學(xué)可能會(huì)遇見這樣的需求:

業(yè)務(wù)端的同學(xué)主線程(或handler線程)發(fā)起異步請(qǐng)求,執(zhí)行完后(通過handler)回調(diào)至主線程(或handler線程)。

在處理這個(gè)問題是,我們也發(fā)現(xiàn) [方案一]中的回調(diào)函數(shù)事實(shí)上也只能在異步線程中執(zhí)行,而不能切換回發(fā)起線程(測(cè)試線程)中執(zhí)行。這顯然不能滿足我們優(yōu)雅的異步接口的測(cè)試需求。于是我們需要新的方案2 Looper 阻塞 通過looper 阻塞 并且實(shí)現(xiàn)回調(diào)函數(shù)的線程切換。

上述問題的根本就是handler 線程的回調(diào)及切換問題,這個(gè)時(shí)候由于測(cè)試線程是沒有l(wèi)ooper 的,我們需要為它營造一個(gè)這樣的環(huán)境。 同時(shí),既然有l(wèi)ooper 的存在, 那么它的自旋功能也就可以滿足我們對(duì)阻塞的需求,這樣的情況下,我們似乎可以直接拋棄掉之前的CountDownLatch了。

于是我們得到了以下代碼


@Runwith(Junit4.calss)

class Test1{

    public static final String TAG="sample test";

    @Test

    public void test1(){

       Looper.prepare();

        //final CountDownLatch mutex = new CountDownLatch(1);

        new YourAsyncJob().run(new YourAsyncTestCallback(){

            @Override

            public void onFinished(){

                Log.i(TAG, "async call back");

                //mutex.countDown();

                Looper.myLooper().quitSafely();

            }

        });

        Log.i(TAG, "run async ok");

        // mutex.await();

        Looper.loop();

    }

}

看起來是可以適應(yīng)這樣的過程,于是開始愉快的測(cè)試起來,但是很快,又遇見了新的問題。

3 Handler thread + 封裝

自動(dòng)化測(cè)試好處在于,自動(dòng)的批量地執(zhí)行測(cè)試case。于是在接下來的過程中我們用到了


    @RunWith(Parameterized.class)

@Parameterized.Parameters 注解來執(zhí)行參數(shù)化的批量輸入。

于是新的問題出現(xiàn)了,由于實(shí)際運(yùn)行時(shí)@Test方法運(yùn)行在同一個(gè)子線程,因此多次Looper.prepare() 顯然是不實(shí)際的,(會(huì)有RuntimeException)。

于是最直接解決的辦法是,一開始prepare好么?

事實(shí)上也不行,這樣的情況回存在如下問題。

何時(shí)執(zhí)行Looper.myLooper().quitSafely()

熟悉Looper 的朋友知道,一旦quit之后,Looper 的queue 將無法使用。 而為了使阻塞的@Test線程恢復(fù)運(yùn)行至結(jié)束,又必須在[方案2]的基礎(chǔ)上解除loop().

于是為了滿足這樣的情況,我們只能通過另起一個(gè)HandlerThread 執(zhí)行這種需要跨線程回調(diào)的接口測(cè)試。然后在回調(diào)執(zhí)行完畢前,阻塞最初的測(cè)試線程@Test線程,保證HandlerThread 的存活。(這里我們每次setup 都會(huì)新起一個(gè)線程,原因是,無法跨TestCase 重用這個(gè)線程,當(dāng)Case執(zhí)行完后,該線程會(huì)被系統(tǒng)強(qiáng)制回收)

于是獲得了如下的內(nèi)容


@RunWith(Parameterized.class)

class Test1{

    public static final String TAG="sample test";

    private HandlerThread t;

    private Handler tH;

    @Parameterized.Parameters

    public static Collection<Object[]> data() {

        //測(cè)試數(shù)據(jù)

        return Arrays.asList(new Object[][]{

                {"TES-1085-7", "TES-1085-7"},

                {null, null},

        });

    }

    @Before

    public void setUp() throws Exception {

          t = new HandlerThread("test");

          t.start();

          tH = new Handler(t.getLooper());

    }

    @Test

    public void test1(){

        final CountDownLatch mutex = new CountDownLatch(1);

        tH.post(new Runnable(){

            @Override

            public void run(){

                new YourAsyncJob().run(new YourAsyncTestCallback(){

                    @Override

                    public void onFinished(){

                        Log.i(TAG, "async call back");

                        mutex.countDown();

                    }

            }

        });

        Log.i(TAG, "run async ok");

        });

       mutex.await();

    }

}

4 優(yōu)化及處理異常

[方案3]基本能夠處理一般的批量測(cè)試。但是作為一個(gè)嚴(yán)謹(jǐn)?shù)某绦騿T,這樣的代碼顯然是不夠優(yōu)雅的。于是我們需要二次封裝,封裝后的代碼調(diào)用會(huì)簡(jiǎn)潔很多,如下


@RunWith(Parameterized.class)

class Test1 extend ZCCBase{

    public static final String TAG="sample test";

    @Parameterized.Parameters

    public static Collection<Object[]> data() {

        //測(cè)試數(shù)據(jù)

        return Arrays.asList(new Object[][]{

                {"TES-1085-7", "TES-1085-7"},

                {null, null},

        });

    }

    @Before

    public void setUp() throws Exception {

         super.setUp();

    }

    @Test

    public void test1(){

        runAsyncTest(new AsyncTest(){

            @Override

            public void onRun(){

                new YourAsyncJob().run(new YourAsyncTestCallback(){

                    @Override

                    public void onFinished(){

                        onAsyncTestFinished();

                    }

            }

        });

    }

}

是不是優(yōu)雅了很多,具體框架和demo使用可以參考 我的github

還沒完,我們還剩下一個(gè)問題。實(shí)際操作時(shí),異步線程中的Assert Error 如果直接拋出的話,并不能在Android Studio 的Run Text 窗口中直接顯示出來,而是會(huì)顯示成 進(jìn)程crash 的日志,真實(shí)原因需要去logcat 中查找。這顯然不是健全的,因此我們還需要把對(duì)應(yīng)的Throwable 拋回測(cè)試線程。 這一功能也已經(jīng)封裝在 我的github中。

That's all, thanks for your time

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第一章:Android基石——四大組件 四大組件:? Activity:負(fù)責(zé)UI元素的加載與頁面之間的跳轉(zhuǎn),相當(dāng)于...
    loneyzhou閱讀 697評(píng)論 0 1
  • 1. ANR異常 Application No Response:應(yīng)用程序無響應(yīng)。在主線程中,是不允許執(zhí)行耗時(shí)的操...
    JackChen1024閱讀 1,586評(píng)論 0 3
  • Android Handler機(jī)制系列文章整體內(nèi)容如下: Android Handler機(jī)制1之ThreadAnd...
    隔壁老李頭閱讀 21,270評(píng)論 13 55
  • 最近工作中遇到一個(gè)比較迷惑的事情,在我利用runtime獲取類的屬性的時(shí)候,由于類實(shí)現(xiàn)了一個(gè)自定義協(xié)議,導(dǎo)致遍歷出...
    JamesYu閱讀 15,983評(píng)論 6 28
  • 憶 少 時(shí) 夜 宿 楊 家 溪 少時(shí)曾隨家父往福鼎白琳老家探親,時(shí)交通閉塞,無車可乘,均靠步行。從霞浦城關(guān)到福鼎白...
    王世榮閱讀 327評(píng)論 2 2

友情鏈接更多精彩內(nèi)容