Android單元測試(六):使用dagger2來作依賴注入,以及在單元測試中的應用

注:html

  1. 代碼中的 //<= 表示新加的、修改的等須要重點關注的代碼java

  2. Class#method表示一個類的instance method,好比 LoginPresenter#login 表示 LoginPresenter的login(非靜態)方法。android

問題

前一篇文章中,咱們講述了依賴注入的概念,以及依賴注入對單元測試極其關鍵的重要性和必要性。在那篇文章的結尾,咱們遇到了一個問題,那就是若是不使用DI框架,而所有采用手工來作DI的話,那麼全部的Dependency都須要在最上層的client來生成,這可不是件好事情。繼續用咱們前面的例子來具體說明一下。
假設有一個登陸界面,LoginActivity,他有一個LoginPresenterLoginPresenter用到了UserManagerPasswordValidator,爲了讓問題變得更明顯一點,咱們假設UserManager用到SharedPreference(用來存儲一些用戶的基本設置等)和UserApiService,而UserApiService又須要由Retrofit建立,而Retrofit又用到OkHttpClient(好比說你要本身控制timeout、cache等東西)。
應用DI模式,UserManager的設計以下:git

public class UserManager {
    private final SharedPreferences mPref;
    private final UserApiService mRestAdapter;

    public UserManager(SharedPreferences preferences, UserApiService userApiService) {
        this.mPref = preferences;
        this.mRestAdapter = userApiService;
    }

    /**Other code*/
}

LoginPresenter的設計以下:程序員

public class LoginPresenter {
    private final UserManager mUserManager;
    private final PasswordValidator mPasswordValidator;

    public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) {
        this.mUserManager = userManager;
        this.mPasswordValidator = passwordValidator;
    }

    /**Other code*/
}

在這種狀況下,最終的client LoginActivity裏面要new一個presenter,須要作的事情以下:github

public class LoginActivity extends AppCompatActivity {
    private LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OkHttpClient okhttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
        Retrofit retrofit = new Retrofit.Builder()
                .client(okhttpClient)
                .baseUrl("https://api.github.com")
                .build();
        UserApiService userApiService = retrofit.create(UserApiService.class);
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        UserManager userManager = new UserManager(preferences, userApiService);

        PasswordValidator passwordValidator = new PasswordValidator();
        mLoginPresenter = new LoginPresenter(userManager, passwordValidator);
    }
}

這個也太誇張了,LoginActivity所須要的,不過是一個LoginPresenter而已,然而它卻須要知道LoginPresenter的Dependency是什麼,LoginPresenter的Dependency的Dependency又是什麼,而後new一堆東西出來。並且能夠預見的是,這個app的其餘地方也須要這裏的OkHttpClientRetrofitSharedPreferenceUserManager等等dependency,所以也須要new這些東西出來,形成大量的代碼重複,和沒必要要的object instance生成。然而如前所述,咱們又必須用到DI模式,這個怎麼辦呢?編程

想一想,若是能達到這樣的效果,那該有多好:咱們只須要在一個相似於dependency工廠的地方統一輩子產這些dependency,以及這些dependency的dependency。全部須要用到這些Dependency的client都從這個工廠裏面去獲取。並且更妙的是,一個client(好比說LoginActivity)只須要知道它直接用到的Dependency(LoginPresenter),而不須要知道它的Dependency(LoginPresenter)又用到哪些Dependency(UserManagerPasswordValidator)。系統自動識別出這個依賴關係,從工廠裏面把須要的Dependency找到,而後把這個client所須要的Dependency建立出來。api

有這樣一個東西,幫咱們實現這個效果嗎?相信聰明的你已經猜到了,回答是確定的,它就是咱們今天要介紹的dagger2。數組

解藥:Dagger2

在dagger2裏面,負責生產這些Dependency的統一工廠叫作 Module ,全部的client最終是要從module裏面獲取Dependency的,然而他們不是直接向module要的,而是有一個專門的「工廠管理員」,負責接收client的要求,而後到Module裏面去找到相應的Dependency,提供給client們。這個「工廠管理員」叫作 Component。基本上,這是dagger2裏面最重要的兩個概念。服務器

下面,咱們來看看這兩個概念,對應到代碼裏面,是怎麼樣的。

生產Dependency的工廠:Module

首先是Module,一個Module對應到代碼裏面就是一個類,只不過這個類須要用dagger2裏面的一個annotation @Module來標註一下,來表示這是一個Module,而不是一個普通的類。咱們說Module是生產Dependency的地方,對應到代碼裏面就是Module裏面有不少方法,這些方法作的事情就是建立Dependency。用上面的例子中的Dependency來講明:

@Module
public class AppModule {

    public OkHttpClient provideOkHttpClient() {
        OkHttpClient okhttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
        return okhttpClient;
    }

    public Retrofit provideRetrofit(OkHttpClient okhttpClient) {
        Retrofit retrofit = new Retrofit.Builder()
                .client(okhttpClient)
                .baseUrl("https://api.github.com")
                .build();
        return retrofit;
    }
}

在上面的Module(AppModule)中,有兩個方法provideOkHttpClient()provideRetrofit(OkHttpClient okhttpClient),分別建立了兩個Dependency,OkHttpClientRetrofit。可是呢,咱們也說了,一個Module就是一個類,這個類有一些生產Dependency的方法,但它也能夠有一些正常的,不是用來生產Dependency的方法。那怎麼樣讓管理員知道,一個Module裏面哪些方法是用來生產Dependency的,哪些不是呢?爲了方便作這個區分,dagger2規定,全部生產Dependency的方法必須用 @Provides這個annotation標註一下。因此,上面的 AppModule正確的寫法應該是:

@Module
public class AppModule {
    @Provides
    public OkHttpClient provideOkHttpClient() {
        OkHttpClient okhttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
        return okhttpClient;
    }

    @Provides
    public Retrofit provideRetrofit(OkHttpClient okhttpClient) {
        Retrofit retrofit = new Retrofit.Builder()
                .client(okhttpClient)
                .baseUrl("https://api.github.com")
                .build();
        return retrofit;
    }
}

這種用來生產Dependency的、用 @Provides修飾過的方法叫Provider方法。這裏要注意第二個Provider方法 provideRetrofit(OkHttpClient okhttpClient),這個方法有一個參數,是OkHttpClient。這是由於建立一個Retrofit對象須要一個OkHttpClient的對象,這裏經過參數傳遞進來。這樣作的好處是,當Client向管理員(Component)索要一個Retrofit的時候,Component會自動找到Module裏面找到生產Retrofit的這個 provideRetrofit(OkHttpClient okhttpClient)方法,找到之後試圖調用這個方法建立一個Retrofit對象,返回給Client。可是調用這個方法須要一個OkHttpClient,因而Component又會去找其餘的provider方法,看看有沒有哪一個會生產OkHttpClient。因而就找到了上面的第一個provider方法: provideOkHttpClient()。找到之後,調用這個方法,建立一個OkHttpClient對象,再調用 provideRetrofit(OkHttpClient okhttpClient)方法,把剛剛建立的OkHttpClient對象傳進去,建立出一個Retrofit對象,返回給Client。固然,若是最後找到的 provideOkHttpClient()方法也須要其餘參數,那麼管理員還會繼續遞歸的找下去,直到全部的Dependency都被知足了,再一個一個建立Dependency,而後把最終Client須要的Dependency呈遞給Client。
很好,如今咱們把文章開頭的例子中的全部Dependency都用這種方式,在 AppModule裏面聲明一個provider方法:

@Module
public class AppModule {
    @Provides
    public OkHttpClient provideOkHttpClient() {
        OkHttpClient okhttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
        return okhttpClient;
    }

    @Provides
    public Retrofit provideRetrofit(OkHttpClient okhttpClient) {
        Retrofit retrofit = new Retrofit.Builder()
                .client(okhttpClient)
                .baseUrl("https://api.github.com")
                .build();
        return retrofit;
    }

    @Provides
    public UserApiService provideUserApiService(Retrofit retrofit) {
        return retrofit.create(UserApiService.class);
    }

    @Provides
    public SharedPreferences provideSharedPreferences(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    @Provides
    public UserManager provideUserManager(SharedPreferences preferences, UserApiService service) {
        return new UserManager(preferences, service);
    }

    @Provides
    public PasswordValidator providePasswordValidator() {
        return new PasswordValidator();
    }

    @Provides
    public LoginPresenter provideLoginPresenter(UserManager userManager, PasswordValidator validator) {
        return new LoginPresenter(userManager, validator);
    }
}

上面的代碼若是你仔細看的話,會發現一個問題,那就是其中的SharedPreference provider方法 provideSharedPreferences(Context context)須要一個context對象,可是 AppModule裏面並無context 的Provider方法,這個怎麼辦呢?對於這個問題,你能夠再建立一個context provider方法,可是context對象從哪來呢?咱們能夠自定義一個Application,裏面提供一個靜態方法返回一個context,這種作法相信你們都幹過。Application類以下:

public class MyApplication extends Application {
    private static Context sContext;

    @Override
    public void onCreate() {
        super.onCreate();
        sContext = this;
    }

    public static Context getContext() {
        return sContext;
    }
}

provider方法以下:

@Provides
    public Context provideContext() {
        return MyApplication.getContext();
    }

可是這種方法不是很好,爲何呢,由於context的得到至關因而寫死了,只能從MyApplication.getContext(),若是測試環境下想把Context換成別的,還要給MyApplication定義一個setter,而後調用MyApplication.setContext(...),這個就繞的有點遠。更好的作法是,把Context做爲 AppModule的一個構造參數,從外面傳進來(應用DI模式,還記得嗎?):

@Module
public class AppModule {
    private final Context mContext;

    public AppModule(Context context) {
        this.mContext = context;
    }

    @Provides
    public Context provideContext() {
        return mContext;
    }

    //其餘的provider方法

}

是的,一個Module就是一個正常的類,它也能夠有構造方法,以及其餘正常類的特性。你可能會想那給構造函數的context對象從哪來呢?別急,這個問題立刻解答。

Dependency工廠管理員:Component

前面咱們講了dagger2的一半,就是生產Dependency的工廠:Module。接下來咱們講另外一半,工廠管理員:Component。跟Module不一樣的是,咱們在實現Component時,不是定義一個類,而是定義一個接口(interface):

public interface AppComponent {
}

名字能夠隨便取,跟Module須要用 @Module修飾一下相似的,一個dagger2的Component須要用 @Component修飾一下,來標註這是一個dagger2的Component,而不是一個普通的interface,因此正確的定義方式是:

@Component
public interface AppComponent {
}

在實際狀況中,可能有多個Module,也可能有多個Component,那麼當Component接收到一個Client的Dependency請求時,它怎麼知道要從哪一個Module裏面去找這些Dependency呢?它不可能遍歷咱們的每個類,而後找出全部的Module,再遍歷全部Module的Provider方法,去找Dependency,這樣先不說能不能作到,就算作獲得,效率也過低了。所以dagger2規定,咱們在定義Component的時候,必須指定這個管理員「管理」哪些工廠(Module)。指定的方法是,把須要這個Component管理的Module傳給 @Component這個註解的modules屬性(或者叫方法?),以下:

@Component(modules = {AppModule.class})  //<=
public interface AppComponent {
}

modules屬性接收一個數組,裏面是這個Component管理的全部Module。在上面的例子中,AppComponent只管理AppModule一個。

Component給Client提供Dependency的方法

前面咱們講了Module和Component的實現,接下來就是Component怎麼給Client提供Dependency的問題了。通常來講,有兩種,固然總共不止這兩種,只不過這兩種最經常使用,也最好理解,通常來講用這兩種就夠了,所以這裏不贅述其餘的方法。

方法一:在Component裏面定義一個返回Dependency的方法

第一種是在Component裏面定義一個返回Dependency的方法,好比LoginActivity須要LoginPresenter,那麼咱們能夠在AppComponent裏面定義一個返回LoginPresenter的方法:

@Component(modules = {AppModule.class})
public interface AppComponent {
    LoginPresenter loginPresenter();
}

你可能會好奇,爲何Component只須要定義成接口就好了,不是應該定義一個類,而後本身使用Module去作這件事嗎?若是是這樣的話,那就太low了。dagger2的工做原理是,在你的java代碼編譯成字節碼的過程當中,dagger2會對全部的Component(就是用 @Component修飾過的interface)進行處理,自動生成一個實現了這個interface的類,生成的類名是Component的名字前面加上「Dagger」。好比咱們定義的 AppComponent,對應的自動生成的類叫作DaggerAppComponent。咱們知道,實現一個interface須要實現裏面的全部方法,所以,DaggerAppComponent是實現了 loginPresenter();這個方法的。實現的方式大體就是從 AppComponent管理的 AppModule裏面去找LoginPresenter的Provider方法,而後調用這個方法,返回一個LoginPresenter

所以,使用這種方式,當Client須要Dependency的時候,首先須要用DaggerAppComponent這個類建立一個對象,而後調用這個對象的 loginPresenter()方法,這樣Client就能得到一個LoginPresenter了,這個DaggerAppComponent對象的建立及使用方式以下:

public class LoginActivity extends AppCompatActivity {
    private LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();  //<=
        mLoginPresenter = appComponent.loginPresenter();   //<=
    }
}

總結一下,咱們到如今爲止,作了什麼:

  1. 咱們定義了一個 AppModule類,裏面定義了一些Provider方法

  2. 定義了一個 AppComponent,裏面定義了一個返回LoginPresenter的方法loginPresenter()

就這樣,咱們即可以使用 DaggerAppComponent.builder().appModule(new AppModule(this)).build().loginPresenter(); 來獲取一個LoginPresenter對象了。
這簡直就是magic,不是嗎?
若是不是dagger2,而是咱們本身來實現這個AppComponent interface,想一想咱們須要作哪些事情:

  1. 定義一個Constructor,接受一個AppModule對象,保存在field中(mAppModule)

  2. 實現loginPresenter()方法,調用mAppModule的provideLoginPresenter(UserManager userManager, PasswordValidator validator)方法,這時候發現這個方法須要兩個參數 UserManagerPasswordValidator

  3. 調用provideUserManager(SharedPreferences preferences, UserApiService service)來獲取一個UserManager,這時候發現這個方法又須要兩個參數 SharedPreferencesUserApiService

  4. 調用provideSharedPreferences(Context context)來獲取一個SharedPreference,這時候發現先要有一個context

  5. 。。。

  6. 。。。

  7. 。。。

說白了,就是把文章開頭咱們寫的那段代碼又實現了一遍,而使用dagger2,咱們就作了前面描述的兩件事而已,這裏面錯綜複雜的Dependency關係dagger2幫咱們自動理清了,生成相應的代碼,去調用相應的Provider方法,知足這些依賴關係。
也許這裏舉得這個例子不足以讓你以爲有什麼大不了的,可是你要知道,一個正常的App,可不只僅有一個Login page而已,稍微大點的App,Dependency都有幾百甚至上千個,對於服務器程序來講,Dependency則更多。對於這點,你們能夠去看Dagger2主要做者的這個視頻,他裏面提到了Google一個android app有3000行代碼專門來管理Dependency,而一個Server app甚至有10萬行這樣的代碼。這個時候要去手動new這些dependency、而且要以正確的順序new出來,簡直會要人命。並且讓問題更加棘手的是,隨着app的演進需求的變動,Dependency之間的關係也在動態的變化。好比說UserManager再也不使用SharedPreference,而是使用database,這個時候UserManager的構造函數裏面少了一個SharedPreferences,多了一個DatabaseHelper這樣的東西,那麼若是使用正常的方式管理Dependency,全部new UserManager的地方都要改,而是用dagger2,你只須要在 AppModule裏面添加一個DatabaseHelper Provider方法,同時把UserManager的provider方法第一參數從SharedPreferences改爲DatabaseHelper就行了,全部用到UserManager的地方不須要作任何更改,LoginPresenter不須要作任何更改,LoginActivity不須要任何更改,這難道不是magic嗎?

說點題外話,這種把問題(咱們這裏是依賴關係)描述出來,而不是把實現過程寫出來的編程風格叫Declarative programming,跟它對應的叫Imperative Programming,相對於後者,前者的優點是:可讀性更高,side effect更少,可擴展性更高等等。這是一種編程風格,跟語言、框架無關。固然,有的語言或框架天生就能讓程序員更容易的使用這種style來編程。這方面最顯著的當屬Prolog,有興趣的能夠去了解下,絕對mind-blowing!
對於Java或Android開發者來講,想讓咱們的代碼更加declarative,最好的方式是使用dagger2和RxJava

方法二:Field Injection

話說回來,咱們繼續介紹dagger2,前面咱們介紹了Component給Client提供Dependency的第一種方式,接下來繼續介紹第二種方式,這種方式叫 Field injection 。這裏咱們繼續用LoginActivity的例子來講明,LoginActivity須要一個LoginPresenter。那麼使用這種方式的作法是,咱們就在LoginActivity裏面定義一個LoginPresenter的field,這個field須要使用 @Inject修飾一下:

public class LoginActivity extends AppCompatActivity {
    @Inject                             //<=
    LoginPresenter mLoginPresenter;     //<=

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

而後在onCreate()裏面,咱們把DaggerAppComponent對象建立出來,調用這個對象的inject方法,把LoginActivity傳進去:

public class LoginActivity extends AppCompatActivity {
    @Inject                             
    LoginPresenter mLoginPresenter;     

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); //<=
        appComponent.inject(this); //<=

        //今後以後,mLoginPresenter就被實例化了
        //mLoginPresenter.isLogin()
    }
}

固然,咱們須要先在AppComponent裏面定義一個inject(LoginActivity loginActivity)方法:

@Component(modules = {AppModule.class})
public interface AppComponent {
    void inject(LoginActivity loginActivity);  //<=
}

DaggerAppComponent實現這個方法的方式是,去LoginActivity裏面全部被 @Inject修飾的field,而後調用 AppModule相應的Provider方法,賦值給這個field。這裏須要注意的是,@Inject field不能使private,否則dagger2找不到這個field。
一般來講,這種方式比第一種方式更簡單,代碼也更簡潔。假設LoginActivity還須要其餘的Dependency,好比須要一個統計打點的Dependency(StatManager),那麼你只須要在AppModule裏面定義一個Provider方法,而後在LoginActivity裏面聲明另一個field就行了:

public class LoginActivity extends AppCompatActivity {
    @Inject                             
    LoginPresenter mLoginPresenter;  

    @Inject
    StatManager mStatManager;   //<=

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();
        appComponent.inject(this);
    }
}

不管有多少個@Inject field,都只須要調用一次appComponent.inject(this);。用過了你就會以爲,恩,好爽!
不過,須要注意的一點是,這種方式不支持繼承,好比說LoginActivity繼承自一個 BaseActivity,而`@Inject
StatManager mStatManager;是放在BaseActivity裏面的,那麼在LoginActivity裏面調用 appComponent.inject(this);並不會讓BaseActivity裏面的 mStatManager獲得實例化,你必須在 BaseActivity裏面也調用一次appComponent.inject(this);`。

@Singleton和Constructor Injection

到這裏,Client從Component獲取Dependency的兩種方式就介紹完畢。可是這裏有個問題,那就是每次Client向Component索要一個Dependency,Component都會建立一個新的出來,這可能會致使資源的浪費,或者說不少時候不是咱們想要的,好比說,SharedPreferencesUserManagerOkHttpClient, Retrofit這些都只須要一份就行了,不須要每次都建立一個instance,這個時候咱們能夠給這些Dependency的Provider方法加上@Singleton就行了。如:

@Module
public class AppModule {

    @Provides
    @Singleton          //<=
    public OkHttpClient provideOkHttpClient() {
        OkHttpClient okhttpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .build();
        return okhttpClient;
    }

    //other method
}

這樣,當Client第一次請求一個OkHttpClient,dagger2會建立一個instance,而後保存下來,下一次Client再次請求一個OkHttpClient是,dagger2會直接返回上次建立好的,而不用再次建立instance。這就至關於用一種更簡便、並且DI-able的方式實現了singleton模式。

這裏再給你們一個bonus,若是你不須要作單元測試,而只是使用dagger2來作DI,組織app的結構的話,其實AppModule裏面的不少Provider方法是不須要定義的。好比說在這種狀況下,LoginPresenter的Provider方法 provideLoginPresenter(UserManager userManager, PasswordValidator validator) 就不須要定義,你只須要在定義LoginPresenter的時候,給它的Constructor加上 @Inject修飾一下:

public class LoginPresenter {
    private final UserManager mUserManager;
    private final PasswordValidator mPasswordValidator;

    @Inject
    public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) {
        this.mUserManager = userManager;
        this.mPasswordValidator = passwordValidator;
    }

    //other methods
}

dagger2會自動建立這個LoginPresenter所須要的Dependency是它可以提供的,因此會去Module裏面找到這個LoginPresenter所需的Dependency,交給LoginPresenter的Constructor,建立好這Dependency,交給Client。這其實也是Client經過Component使用Dependency的一種方式,叫 Constructor injection (上一篇文章也提到Constructor injection,不過稍微有點不一樣,注意區分一下)一樣的,在那種狀況下,UserManager的Provider方法也不須要定義,而只須要給UserManager的Constructor加上一個@Inject就行了。說白了,你只須要給那些不是經過Constructor來建立的Dependency(好比說SharedPreferences、UserApiService等)定義Provider方法。
有了 Constructor injection ,咱們的代碼又能獲得進一步的簡化,然而遺憾的是,這種方式將致使咱們作單元測試的時候沒法mock這中間的Dependency。說到單元測試,咱們別忘了這個系列的主題T_T。。。那麼接下來就介紹dagger2在單元測試裏面的使用,以及爲何 Constructor injection 將致使單元測試裏面沒法mock這個Dependency。

dagger2在單元測試裏面的使用

在介紹dagger2在單元測試裏面的使用以前,咱們先改進一下前面的代碼,咱們建立DaggerAppComponent的地方是在LoginActivity,其實這樣不是很好,爲何呢?想一想若是login之後其餘地方也須要UserManager,那麼咱們又要建立一個DaggerAppComponent,這種地方是不少的,畢竟 AppModule裏面定義了一些整個app都要用到的Dependency,好比說Retrofit、SharedPreferences等等。若是每一個須要用到的地方都建立一遍DaggerAppComponent,就致使了代碼的重複和內存性能的浪費,理論上來講,DaggerAppComponent對象整個app只須要一份就行了。因此咱們在用dagger2的時候,通常的作法是,咱們會在app啓動的時候建立好,放在某一個地方。好比,咱們在自定義的Application#onCreate()裏面建立好,而後放在某個地方,我我的習慣定義一個類叫ComponentHolder,而後放裏面:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        AppComponent appComponent = DaggerAppComponent.builder()
                                    .appModule(new AppModule(this))
                                    .build();
        ComponentHolder.setAppComponent(appComponent);
    }
}

public class ComponentHolder {
    private static AppComponent sAppComponent;

    public static void setAppComponent(AppComponent appComponent) {
        sAppComponent = appComponent;
    }

    public static AppComponent getAppComponent() {
        return sAppComponent;
    }
}

而後在須要 AppComponent的地方,使用 ComponentHolder.getAppComponent()來獲取一個DaggerAppComponent對象:

public class LoginActivity extends AppCompatActivity {
    @Inject
    LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ComponentHolder.getAppComponent().inject(this);  //<=
    }
}

這樣在用的地方,看起來代碼也乾淨了不少。
到這裏,咱們就能夠介紹在單元測試裏面怎麼來mock Dependency了。假設LoginActivity有兩個EditText和一個login button,點擊這個button,將從兩個EditText裏面獲取用戶名和密碼,而後調用LoginPresenter的login方法:

public class LoginActivity extends AppCompatActivity {
    @Inject
    LoginPresenter mLoginPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ComponentHolder.getAppComponent().inject(this);

        findViewById(R.id.login).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                String username = ((EditText) findViewById(R.id.username)).getText().toString();
                String password = ((EditText) findViewById(R.id.password)).getText().toString();

                mLoginPresenter.login(username, password);
            }
        });
    }
}

咱們如今要測的,就是當用戶點擊這個login button的時候,mLoginPresenter的login方法獲得了調用,若是你看了這個系列的前面幾篇文章,你就知道這裏的mLoginPresenter須要mock掉。可是,這裏的mLoginPresenter是從dagger2的component裏面獲取的,這裏怎麼把mLoginPresenter換成mock呢?
咱們在回顧一下,其實LoginActivity只是向DaggerAppComponent索取了一個LoginPresenter,而DaggerAppComponent實際上是調用了AppModuleprovideLoginPresenter()方法來得到了一個LoginPresenter,返回給LoginActivity,也就是說,真正生產LoginPresenter的地方是在 AppModule。還記得嗎,咱們建立DaggerAppComponent的時候,給它的builder傳遞了一個AppModule對象:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        AppComponent appComponent = DaggerAppComponent.builder()
                                    .appModule(new AppModule(this)) //<= 這裏這裏
                                    .build();
        ComponentHolder.setAppComponent(appComponent);
    }
}

其實DaggerAppComponent調用的AppModule對象,就是咱們在建立它的時候傳給那個builder的。那麼,若是咱們傳給DaggerAppComponentAppModule是一個mock對象,在這個mock對象的provideLoginPresenter()被調用的時候,返回一個mock的LoginPresenter,那麼LoginActivity得到的,不就是一個mock的LoginPresenter了嗎?
咱們用代碼來實現一下看看是什麼樣子,這裏由於LoginActivity是android相關的類,所以須要用到robolectric這個framework,雖然這個咱們尚未介紹到,可是代碼應該看得懂,以下:

@RunWith(RobolectricGradleTestRunner.class) //Robolectric相關,看不懂的話忽略
@Config(constants = BuildConfig.class, sdk = 21) //同上
public class LoginActivityTest {

    @Test
    public void testActivityStart() {
        @Test
        public void testActivityStart() {
            AppModule mockAppModule = spy(new AppModule(RuntimeEnvironment.application)); //建立一個mockAppModule,這裏不能spy(AppModule.class),由於`AppModule`沒有默認無參數的Constructor,也不能mock(AppModule.class),緣由是dagger2的約束,Provider方法不能返回null,除非用@Nullable修飾
            LoginPresenter mockLoginPresenter = mock(LoginPresenter.class);  //建立一個mockLoginPresenter
            Mockito.when(mockAppModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(mockLoginPresenter);  //當mockAppModule的provideLoginPresenter()方法被調用時,讓它返回mockLoginPresenter
            AppComponent appComponent = DaggerAppComponent.builder().appModule(mockAppModule).build();  //用mockAppModule來建立DaggerAppComponent
            ComponentHolder.setAppComponent(appComponent);  //記得放到ComponentHolder裏面,這樣LoginActivity#onCreate()裏面經過ComponentHolder.getAppComponent()得到的就是這裏建立的appComponent

            LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class); //啓動LoginActivity,onCreate方法會獲得調用,裏面的mLoginPresenter經過dagger2得到的,將是mockLoginPresenter
            ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang");
            ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome");
            loginActivity.findViewById(R.id.login).performClick();    

            verify(mockLoginPresenter).login("xiaochuang", "xiaochuang is handsome");  //pass!
        }
    }
}

這就是dagger2在單元測試裏面的應用。基本上就是mock Module的Provider方法,讓它返回你想要的mock對象。這也解釋了爲何說只用 Constructor injection 的話,會致使Dependency沒法mock,由於沒有對應的Provider方法來讓咱們mock啊。上面的代碼看起來也許你會以爲有點多,然而實際開發中,上面測試方法裏的第一、四、5行都是通用的,咱們能夠把他們抽到一個輔助類裏面:

public class TestUtils {
   public static final AppModule appModule = spy(new AppModule(RuntimeEnvironment.application));

   public static void setupDagger() {
       AppComponent appComponent = DaggerAppComponent.builder().appModule(appModule).build();
       ComponentHolder.setAppComponent(appComponent);
   }
}

這樣咱們前面的測試方法就能夠簡化了:

public class LoginActivityTest {

    @Test
    public void testActivityStart() {
        TestUtils.setupDagger();
        LoginPresenter mockLoginPresenter = mock(LoginPresenter.class);
        Mockito.when(TestUtils.appModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(mockLoginPresenter);

        LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);
        ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang");
        ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome");
        loginActivity.findViewById(R.id.login).performClick();

        verify(mockLoginPresenter).login("xiaochuang", "xiaochuang is handsome");
    }
}

固然,上面的代碼還能夠用不少種方法做進一步簡化,好比把TestUtils.setupDagger();放到@Before裏面,或者是自定義一個基礎測試類,把TestUtils.setupDagger();放這個基礎測試類的@Before裏面,而後 LoginActivityTest繼承這個基礎測試類就能夠了,or even better,自定義一個JUnit Rule,在每一個測試方法被調用以前自動調用 TestUtils.setupDagger();。只是這些與當前的主題無關,就不具體展開敘述了。後面會講到DaggerMock的使用,這個東西可真的是神器啊!簡直不要太神器!

單元測試裏面,不要濫用dagger2

這裏再重複一下上一篇文章的話,單元測試的時候不要濫用dagger2,雖然如今咱們的app是用dagger2架構起來的,全部的Dependency都是在Module裏面生產,但並不表明咱們在作單元測試的時候,這些Dependency也只能在Module裏面生產。好比說,LoginPresenter

public class LoginPresenter {
    private final UserManager mUserManager;
    private final PasswordValidator mPasswordValidator;

    @Inject
    public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) {
        this.mUserManager = userManager;
        this.mPasswordValidator = passwordValidator;
    }

    public void login(String username, String password) {
        if (username == null || username.length() == 0) return;
        if (mPasswordValidator.verifyPassword(password)) return;

        mUserManager.performLogin(username, password);
    }
}

咱們要測的是,LoginPresenter#login()調用了 mUserManager.performLogin()。在這裏,咱們能夠按照上面的思路,使用dagger2來mock UserManager,作法是mock module 的provideUserManager()方法,讓它返回一個mock的 UserManager,而後去verify這個mock UserManagerperformLogin()方法獲得了調用,代碼大體以下:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class LoginPresenterTest {

    @Test
    public void testLogin() throws Exception {
        TestUtils.setupDagger();
        UserManager mockUserManager = mock(UserManager.class);
        Mockito.when(TestUtils.appModule.provideUserManager(any(SharedPreferences.class), any(UserApiService.class))).thenReturn(mockUserManager);

        LoginPresenter presenter = ComponentHolder.getAppComponent().loginPresenter();
        presenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

這樣雖然能夠,並且也不難,但畢竟路繞的有點遠,並且你可能要作額外的一些工做,好比在AppComponent裏面加一個正式代碼不必定會用的 loginPresenter();方法,另外由於AppModule裏面有安卓相關的代碼,咱們還必須使用Robolectric,致使測試跑起來慢了不少。其實咱們徹底能夠不用dagger2,有更好的辦法,那就是直接new LoginPresenter,傳入mock UserManager

public class LoginPresenterTest {
    @Test
    public void testLogin() {
        UserManager mockUserManager = mock(UserManager.class);
        LoginPresenter presenter = new LoginPresenter(mockUserManager, new PasswordValidator()); //由於這裏咱們不verify PasswordValidator,因此不須要mock這個。

        presenter.login("xiaochuang", "xiaochuang is handsome");

        verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome");
    }
}

程序是否是簡單多了,也容易理解多了?
那麼如今問題來了,若是這樣的話,單元測試的時候,哪些狀況應該用dagger2,那些狀況不用呢?答案是,能不用dagger2,就不用dagger2,不得已用dagger2,才用dagger2。固然,這是一句廢話,前面咱們已經明顯感覺到了,在單元測試裏面用dagger2比不用dagger2要麻煩多了,能不用固然不用。那麼問題就變成了,什麼狀況下必須用dagger二、而何時能夠不用呢?答案是,若是被測類(好比說LoginActivity)的Dependency(LoginPresenter)是經過 field injection inject進去的,那麼再測這個類(LoginActivity)的時候,就必須用dagger2,否則很難優雅的把mock傳進去。相反,若是被測類有Constructor(好比說LoginPresenter),Dependency是經過Constructor傳進去的,那麼就能夠不使用dagger2,而是直接new對象出來測。這也是爲何我在前一篇文章裏面強烈的推薦 Constructor Injection的緣由。

小結

這篇文章介紹了dagger2的使用,以及在單元測試裏面的應用。哦好像忘了介紹把dagger2加到項目裏面的方法,其實很簡單,把如下代碼加入build.gradle:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

apply plugin: 'com.android.application'  //這個已經有了,這裏只是想說明要把android-apt這個plugin放到這個的後面。
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    //other dependencies

    //Dagger2
    compile 'com.google.dagger:dagger:2.0.2'
    compile 'javax.annotation:jsr250-api:1.0'
    apt 'com.google.dagger:dagger-compiler:2.0.2'
}

應該說,DI是一種很好的模式,哪怕不作單元測試,DI也會讓咱們的app的架構變得乾淨不少,可讀性、維護性和可拓展性強不少,只不過單元測試讓DI的必要性變得更加顯著和迫切而已。而dagger2的做用,或者說角色,在於它讓咱們寫正式代碼的時候使用DI變得易如反掌,程序及其簡潔優雅可讀性高。同時,它在某些狀況下讓原來很難測的代碼變得用以測試。

文中的代碼在github這個項目裏面
最後,若是你也對安卓單元測試感興趣的話,歡迎加入咱們的交流羣:

參考:https://www.youtube.com/watch?v=oK_XtfXPkqw

做者 小創 更多文章 | Github | 公衆號

相關文章
相關標籤/搜索