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 | 公众号

相关文章
相关标签/搜索