使用这种方式,所有production code都不用专门为testing增加任何多余的代码,同时还能得到依赖注入的其他好处。
Robolectric:解决Android单元测试最大的痛点
接下来讲讲Android单元测试最大的痛点,那就是JVM上面运行纯JUnit单元测试时是不能使用Android相关的类的,因为我们开发用到的安卓环境是没有实现的,里面只定义了一些接口,所有方法的实现都是throw new RuntimeException("stub");,如果我们单元测试代码里面用到了安卓相关的代码的话,那么运行时就会遇到RuntimeException("Stub")。
要解决这个问题,一般来说有三种方案:
使用Android提供的Instrumentation系统,将单元测试代码运行在模拟器或者是真机上。
用一定的架构,比如MVP等等,将安卓相关的代码隔离开了,中间的Presenter或Model是存java实现的,可以在JVM上面测试。View或其他android相关的代码则不测。
使用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
原文转自:http://www.jianshu.com/p/9f7a992fe9ec