Android單元測(cè)試(八):怎樣測(cè)試異步代碼

異步無(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í)際上攘须,可以使用synchronizedwait/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è)試異步代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末雅倒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子弧可,更是在濱河造成了極大的恐慌蔑匣,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棕诵,死亡現(xiàn)場(chǎng)離奇詭異裁良,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)校套,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)趴久,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人搔确,你說(shuō)我怎么就攤上這事。” “怎么了膳算?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵座硕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我涕蜂,道長(zhǎng)华匾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任机隙,我火速辦了婚禮蜘拉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘有鹿。我一直安慰自己旭旭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布葱跋。 她就那樣靜靜地躺著持寄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娱俺。 梳的紋絲不亂的頭發(fā)上稍味,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音荠卷,去河邊找鬼模庐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛油宜,可吹牛的內(nèi)容都是我干的掂碱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼验庙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼顶吮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起粪薛,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悴了,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后违寿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體湃交,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年藤巢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搞莺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掂咒,死狀恐怖才沧,靈堂內(nèi)的尸體忽然破棺而出迈喉,到底是詐尸還是另有隱情,我是刑警寧澤温圆,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布挨摸,位于F島的核電站,受9級(jí)特大地震影響岁歉,放射性物質(zhì)發(fā)生泄漏得运。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一锅移、第九天 我趴在偏房一處隱蔽的房頂上張望熔掺。 院中可真熱鬧,春花似錦非剃、人聲如沸置逻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)诽偷。三九已至,卻和暖如春疯坤,著一層夾襖步出監(jiān)牢的瞬間报慕,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工压怠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留眠冈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓菌瘫,卻偏偏與公主長(zhǎng)得像蜗顽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子雨让,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345