那么这里我介绍一下,怎么样把Dagger2应用到单元测试中。熟悉dagger2的童靴可能知道,Dagger2里面关键的有两个概念,Module 和Component。Module是负责生成诸如DataModel这样被别人(比如DataActivity)使用的类的地方。用术语的话,被别人使用的类DataModel叫Dependency,使用到了别的类的类DataActivity叫Client。而Component则是供Client使用Dependency的统一接口。也是说,DataActivity通过Component,来得到一份DataModel的实例。
  现在,关键的地方来了,Component本身是不生产dependency的,它只是搬运工而已,真正生产dependency的地方在Module。所以,创建Component需要用到Module,不同的Module生产出不同的dependency。在正式代码里面,我们使用正常的Module,生产正常的DataModel。而在测试环境中,我们写一个TestingModule,让它继承正常的Module,然后override掉生产DataModel的方法,让它生产mock的DataModel。在跑单元测试的时候,使用这个TestingModule来创建Component,这样的话,DataActivity通过Component得到的DataModel对象是mock出来的DataModel对象。
  使用这种方式,所有production code都不用专门为testing增加任何多余的代码,同时还能得到依赖注入的其他好处。
  Robolectric:解决Android单元测试大的痛点
  接下来讲讲Android单元测试大的痛点,那是JVM上面运行纯JUnit单元测试时是不能使用Android相关的类的,因为我们开发用到的安卓环境是没有实现的,里面只定义了一些接口,所有方法的实现都是throw new RuntimeException("stub");,如果我们单元测试代码里面用到了安卓相关的代码的话,那么运行时会遇到RuntimeException("Stub")。
  要解决这个问题,一般来说有三种方案:
  1、使用Android提供的Instrumentation系统,将单元测试代码运行在模拟器或者是真机上。
  2、用一定的架构,比如MVP等等,将安卓相关的代码隔离开了,中间的Presenter或Model是存java实现的,可以在JVM上面测试。View或其他android相关的代码则不测。
  3、使用Robolectric框架,这个框架基本可以理解为在JVM上面实现了一套安卓的模拟环境,同时给安卓相关的类增加了其他一些增强的功能,以方便做单元测试,使用这个框架,我们可以在JVM上面跑单元测试的时候,可以使用安卓相关的类了。
  第一种方案能work,但是速度非常慢,因为每次运行一次单元测试,都需要将整个项目打包成apk,上传到模拟器或真机上,跟运行了一次app似得,这个显然不是单元测试该有的速度,更无法做TDD。这种方案首先被否决。
  刚开始,我们采用的是Robolectric,原因有两个:1. 我们项目当时还没有比较清楚的架构,android跟纯java代码的隔离没有做好;2. 很多安卓相关的代码,还是需要测试的,比如说自定义View等等。然而慢慢的,我们的态度从拥抱Robolectric,到尽量不用它,尽量使用纯java代码去实现。可能大家觉得安卓相关的代码会很多,而纯java的很少,然而慢慢的你会发现,其实不是这样的,纯java的代码其实真不少,而且往往是核心的逻辑所在。之所以尽量不用Robolectric,是因为Robolectric虽然相对于Instrumentation testing来说快多了。但毕竟他也需要merge一些资源,build出来一个模拟的app,因此相对于纯java和JUnit来说,这个速度依然是很慢的。
  用具体的数字来对比说明:
  · 运行Instrumentation testing:几十秒,取决于app的大小
  · Robolectric:10秒左右
  · JUnit:几秒钟之内
  当然,虽然运行一次Robolectric在10秒左右,但是对比运行一次app,还是要快太多。因此,刚开始的时候,从Robolectric开始完全是OK的。
  以上是现在我们这边单元测试用到的几个基本技术:JUnit4 + Mockito + Dagger2 + Robolectric。基本来说,并没有什么黑科技,都是业界标准。
  一个具体的案例
  接下来,我通过一个具体的案例,跟大家介绍一下,我们这边的一个app,具体是怎么单测的。
  这里是我们收银台界面的样子:

  假设Activity名字为CheckoutActivity,当它启动的时候,CheckoutActivity会去调一个CheckoutModel的loadCheckoutData()方法,这个方法又会去调更底层的一个封装了用户认证等信息的网络请求Api类(mApi)的get方法,同时传给这个Api类一个callback。这个callback的做的事情是将结果通过Otto Bus(mBus) post出去。CheckoutActivity里面Subscribe了这个Event(方法名是onCheckoutDataLoaded()),然后根据Event的值相应的显示数据或错误信息。
  代码简写如下:
public class CheckoutActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// other code, like setContentView, get data from Intent, etc.
mCheckoutModel.loadCheckoutData(paymentId);
}
@Subscribe
public void onCheckoutDataLoaded(DataLoadedEvent event) {
if (event.successful()) {
//Get data from event and update UI
} else {
//show error message
}
}
}
public class CheckoutModel {
public void loadCheckoutData(String paymentId) {
//Other code, like composing params
mApi.get(someUrl, someParams, new NetworkCallback() {
@Override
public void onSuccess(Object data) {
mBus.post(new DataLoadedEvent(data));
}
@Override
public void onFailure(int code, String msg) {
mBus.post(new DataLoadedEvent(code, msg));
}
});
}
}
  这里,CheckoutActivity里面的mCheckoutModel、CheckoutModel里面的mApi、CheckoutModel里面的mBus,都是通过Dagger2注入进去的。在做单元测试的时候,这些都是mock。