Android單元測試(五):依賴注入,將mock方便的用起來

上一篇文章中,我們講了要將mock出來的dependency真正使用起來,需要在測試環(huán)境下通過某種方式set 到用到它的那個對象里面進(jìn)去,替換掉真實的實現(xiàn)。我們前面舉的例子是:

public class LoginPresenter {
    private UserManager mUserManager = new UserManager();

    public void login(String username, String password) {
        //。。。some other code
        mUserManager.performLogin(username, password);
    }
}

在測試LoginPresenter#login()時,為了能夠?qū)ock出來的UserManager set到LoginPresenter里面,我們前面的做法是簡單粗暴,給LoginPresenter加一個UserManager的setter。然而這種做法畢竟不是很優(yōu)雅,一般來說,我們正式代碼里面是不會去調(diào)用這個setter,修改UserManager這個對象的。因此這個setter存在的意義就純粹是為了方便測試。這個雖然不是沒有必要,卻不是太好看,因此在有選擇的情況下,我們不這么做。在這里,我們介紹依賴注入這種模式。

對于依賴注入(Dependency Injection,以下簡稱DI)的準(zhǔn)確定義可以在這里找到。它的基本理念這邊簡單描述下,首先這是一種代碼模式,這個模式里面有兩個概念:Client和Dependency。假如你的代碼里面,一個類用到了另外一個類,那么前者叫Client,后者叫Dependency。結(jié)合上面的例子,LoginPresenter用到了UserManager,那么LoginPresenter叫Client,UserManager叫Dependency。當(dāng)然,這是個相對的概念,一個類可以是某個類的Dependency,卻是另外一個類的Client。比如說如果UserManager里面用到了Retrofit,那么相對于Retrofit,UserManager又是Client。DI的基本思想就是,對于Dependency的創(chuàng)建過程,并不在Client里面進(jìn)行,而是由外部創(chuàng)建好,然后通過某種方式set到Client里面。這種模式,就叫做依賴注入。

是的,依賴注入就是這么簡單的一個概念,這邊需要澄清的一點是,這個概念本身跟dagger2啊,RoboGuice這些框架并沒有什么關(guān)系?,F(xiàn)在很多介紹DI的文章往往跟dagger2是在一起的,因為dagger2的使用相對來說不是很直觀,所以導(dǎo)致很多人認(rèn)為DI是多么復(fù)雜的東西,甚至認(rèn)為只能用dagger等框架來實現(xiàn)依賴注入,其實不是這樣的。實現(xiàn)依賴注入很簡單,dagger這些框架只是讓這種實現(xiàn)變得更加簡單,簡潔,優(yōu)雅而已。

DI的常見實現(xiàn)方式

下面介紹DI的實現(xiàn)方式,通常來說,這里是大力介紹dagger2的地方。但是,雖然dagger2的確是非常好的東西,然而如果我直接介紹dagger2的話,會很容易導(dǎo)致一個誤區(qū),認(rèn)為在測試的時候,也只能用dagger來做依賴注入或創(chuàng)建對應(yīng)的測試類,因此,我這邊刻意不介紹dagger。先讓大家知道最基本的DI怎么實現(xiàn),然后在測試的時候如何更方便高效的使用。

實現(xiàn)DI這種模式其實很簡單,有多種方式,上一篇文章中提到的setter,其實就是實現(xiàn)DI的一種方式,叫做 setter injection 。此外,通過方法的參數(shù)傳遞進(jìn)去(argument injection),也是實現(xiàn)DI的一種方式:

public class LoginPresenter {

    //這里,LoginPresenter不再持有UserManager的一個引用,而是作為方法參數(shù)直接傳進(jìn)去
    public void login(UserManager userManager, String username, String password) {
        //... some other code
        userManager.performLogin(username, password);
    }
}

然而更常用的方式,是將Dependency作為Client的構(gòu)造方法的參數(shù)傳遞進(jìn)去:

public class LoginPresenter {

    private final UserManager mUserManager;

    //將UserManager作為構(gòu)造方法參數(shù)傳進(jìn)來
    public LoginPresenter(UserManager userManager) {
        this.mUserManager = userManager;
    }

    public void login(String username, String password) {
        //... some other code
        mUserManager.performLogin(username, password);
    }
}

這種實現(xiàn)DI的模式叫 Constructor Injection。其實一般來說,提到DI,指的都是這種方式。這種方式的好處是,依賴關(guān)系非常明顯。你必須在創(chuàng)建這個類的時候,就提供必要的dependency。這從某種程度上來說,也是在說明這個類所完成的功能。因此,盡量使用 Constructor injection

說到這里,你可能會有一個疑問,如果把依賴都聲明在Constructor的參數(shù)里面,這會不會讓這個類的Constructor參數(shù)變得非常多?如果真的發(fā)生這種情況了,那往往說明這個類的設(shè)計是有問題的,需要重構(gòu)。為什么呢?我們代碼里面的類,一般可以分為兩種,一種是Data類,比如說UserInfo,OrderInfo等等。另外一種是Service類,比如UserManager, AudioPlayer等等。所以這個問題就有兩種情況了:

  1. 如果Constructor里面?zhèn)魅氲暮芏嗍腔绢愋偷臄?shù)據(jù)或數(shù)據(jù)類,那么或許你要做的,是創(chuàng)建一個(或者是另一個)數(shù)據(jù)類把這些數(shù)據(jù)封裝一下,這個過程的價值可是大大滴!而不僅僅是封裝一下參數(shù)的問題,有了一個類,很多的方法就可以放到這個類里面了。這點請參考Martin Fowler的《重構(gòu)》第十章“Introduce Parameter Object”。
  2. 如果傳入的很多是service類,那么這說明這個類做的事情太多了,不符合單一職責(zé)的原則(Single Responsibility Principle,SRP),因此,需要重構(gòu)。

接下來說回我們的初衷:DI在測試?yán)锩娴膽?yīng)用。

DI在單元測試?yán)锩娴膽?yīng)用

所謂DI在單元測試?yán)锩娴膽?yīng)用,其實說白了就是使用DI模式,將mock出來的Dependency set到Client里面去。我相信這篇文章解釋到這里,那么答案也就比較明顯了,為了強(qiáng)調(diào)我們要盡量使用 Constructor injection,對于 setter InjectionArgument injection 這邊就不做代碼示例了。
如果你的代碼使用的是 Constructor injection

public class LoginPresenter {

    private final UserManager mUserManager;

    //將UserManager作為構(gòu)造方法參數(shù)傳進(jìn)來
    public LoginPresenter(UserManager userManager) {
        this.mUserManager = userManager;
    }

    public void login(String username, String password) {
        //... some other code
        mUserManager.performLogin(username, password);
    }
}

其中我們要測的方法是login(), 要驗證login()方法調(diào)用了mUserManagerperformLigon()。對應(yīng)的測試方法如下:

public class LoginPresenterTest {

    @Test
    public void testLogin() {
        UserManager mockUserManager = Mockito.mock(UserManager.class);
        LoginPresenter presenter = new LoginPresenter(mockUserManager);  //創(chuàng)建的時候,講mock傳進(jìn)去

        presenter.login("xiaochuang", "xiaochuang password");

        Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
    }
}

很簡單,對吧。

小結(jié)

這篇文章介紹了DI的概念,以及在單元測試?yán)锩娴膽?yīng)用,這里特意沒有介紹dagger2的使用,目的是要強(qiáng)調(diào):

  1. 一個靈活的,易于測試的,符合SRP的,結(jié)構(gòu)清晰的項目,關(guān)鍵在于要應(yīng)用依賴注入這種模式,而不是用什么來做依賴注入。
  2. 等你學(xué)會使用dagger以后,要記得在測試的時候,如果可以直接mock dependency并傳給被測類,那就直接創(chuàng)建,不是一定要使用dagger來做DI

然而如果完全不使用框架來做DI,那么在正式代碼里面就有一個問題了,那就是dependency的創(chuàng)建工作就交給上層client去處理了,這可不是件好事情。想想看,LoginActivity里面創(chuàng)建LoginPresenter的時候,還得知道LoginPresenter用了UserManager。然后創(chuàng)建一個UserManager對象給LoginPresenter。對于LoginActivity來說,它覺得我才懶得管你用什么樣的UserManager呢,我只想告訴你login的時候,你給我老老實實的login就好了,你用什么Manager我不管。所以,直接在LoginActivity里面創(chuàng)建UserManager,可能不是個好的選擇。那怎么樣算是一個好的選擇呢?dagger2給了我們答案。
于是下一篇文章我們介紹dagger2。

文中的代碼在github這個項目里面。

最后,如果你對安卓單元測試感興趣,歡迎加入我們的交流群,因為群成員超過100人,沒辦法掃碼加入,請關(guān)注下方公眾號獲取加入方法。

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

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,324評論 25 708
  • 小t一直不知道家鄉(xiāng)到底是什么樣子。 不是從小漂泊流浪在外的孩子,也不是被鄰居街坊看不上眼的小子,說來倒也還是一貫被...
    Laeddis閱讀 298評論 0 2
  • 前幾天感恩節(jié),公司的同事在筆耕不輟的寫著信封,寄給千里之外的,離我們很遠(yuǎn)也很近的會員。 陳童說,這是一封有愛的信。...
    lemon若塵閱讀 1,860評論 0 0
  • 蝴蝶能攪亂眼睛 無話不談的人多含辛 如果飛蛾能逃離煙熏 你說沒有念念不忘 我就信 你說故鄉(xiāng)有眼睛 黑白分明 那分明...
    池囊閱讀 185評論 1 1
  • ? 當(dāng)列車在躁動中狂奔時,窗外的群山一直保持靜默,這正是我們對她的敬畏。 ...
    烽火煤閱讀 172評論 0 1

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