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

小創發表於2016-05-09

在上一篇文章中,我們講了要將mock出來的dependency真正使用起來,需要在測試環境下通過某種方式set 到用到它的那個物件裡面進去,替換掉真實的實現。我們前面舉的例子是:

在測試LoginPresenter#login()時,為了能夠將mock出來的UserManager set到LoginPresenter裡面,我們前面的做法是簡單粗暴,給LoginPresenter加一個UserManager的setter。然而這種做法畢竟不是很優雅,一般來說,我們正式程式碼裡面是不會去呼叫這個setter,修改UserManager這個物件的。因此這個setter存在的意義就純粹是為了方便測試。這個雖然不是沒有必要,卻不是太好看,因此在有選擇的情況下,我們不這麼做。在這裡,我們介紹依賴注入這種模式。

對於依賴注入(Dependency Injection,以下簡稱DI)的準確定義可以在這裡找到。它的基本理念這邊簡單描述下,首先這是一種程式碼模式,這個模式裡面有兩個概念:Client和Dependency。假如你的程式碼裡面,一個類用到了另外一個類,那麼前者叫Client,後者叫Dependency。結合上面的例子,LoginPresenter用到了UserManager,那麼LoginPresenter叫Client,UserManager叫Dependency。當然,這是個相對的概念,一個類可以是某個類的Dependency,卻是另外一個類的Client。比如說如果UserManager裡面用到了Retrofit,那麼相對於RetrofitUserManager又是Dependency。DI的基本思想就是,對於Dependency的建立過程,並不在Client裡面進行,而是由外部建立好,然後通過某種方式set到Client裡面。這種模式,就叫做依賴注入。

是的,依賴注入就是這麼簡單的一個概念,這邊需要澄清的一點是,這個概念本身跟dagger2啊,RoboGuice這些框架並沒有什麼關係。現在很多介紹DI的文章往往跟dagger2是在一起的,因為dagger2的使用相對來說不是很直觀,所以導致很多人認為DI是多麼複雜的東西,甚至認為只能用dagger等框架來實現依賴注入,其實不是這樣的。實現依賴注入很簡單,dagger這些框架只是讓這種實現變得更加簡單,簡潔,優雅而已。

DI的常見實現方式

下面介紹DI的實現方式,通常來說,這裡是大力介紹dagger2的地方。但是,雖然dagger2的確是非常好的東西,然而如果我直接介紹dagger2的話,會很容易導致一個誤區,認為在測試的時候,也只能用dagger來做依賴注入或建立對應的測試類,因此,我這邊刻意不介紹dagger。先讓大家知道最基本的DI怎麼實現,然後在測試的時候如何更方便高效的使用。

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

然而更常用的方式,是將Dependency作為Client的構造方法的引數傳遞進去:

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

說到這裡,你可能會有一個疑問,如果把依賴都宣告在Constructor的引數裡面,這會不會讓這個類的Constructor引數變得非常多?如果真的發生這種情況了,那往往說明這個類的設計是有問題的,需要重構。為什麼呢?我們程式碼裡面的類,一般可以分為兩種,一種是Data類,比如說UserInfo,OrderInfo等等。另外一種是Service類,比如UserManager, AudioPlayer等等。所以這個問題就有兩種情況了:

  1. 如果Constructor裡面傳入的很多是基本型別的資料或資料類,那麼或許你要做的,是建立一個(或者是另一個)資料類把這些資料封裝一下,這個過程的價值可是大大滴!而不僅僅是封裝一下引數的問題,有了一個類,很多的方法就可以放到這個類裡面了。這點請參考Martin Fowler的《重構》第十章“Introduce Parameter Object”。
  2. 如果傳入的很多是service類,那麼這說明這個類做的事情太多了,不符合單一職責的原則(Single Responsibility Principle,SRP),因此,需要重構。

接下來說回我們的初衷:DI在測試裡面的應用。

DI在單元測試裡面的應用

所謂DI在單元測試裡面的應用,其實說白了就是使用DI模式,將mock出來的Dependency set到Client裡面去。我相信這篇文章解釋到這裡,那麼答案也就比較明顯了,為了強調我們要儘量使用 Constructor injection,對於 setter InjectionArgument injection 這邊就不做程式碼示例了。 如果你的程式碼使用的是 Constructor injection

其中我們要測的方法是login(), 要驗證login()方法呼叫了mUserManagerperformLigon()。對應的測試方法如下:

很簡單,對吧。

小結

這篇文章介紹了DI的概念,以及在單元測試裡面的應用,這裡特意沒有介紹dagger2的使用,目的是要強調:

  1. 一個靈活的,易於測試的,符合SRP的,結構清晰的專案,關鍵在於要應用依賴注入這種模式,而不是用什麼來做依賴注入。
  2. 等你學會使用dagger以後,要記得在測試的時候,如果可以直接mock dependency並傳給被測類,那就直接建立,不是一定要使用dagger來做DI

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

文中的程式碼在github這個專案裡面

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

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

相關文章