Android单元测试在蘑菇街支付金融部门的实践(3)

发表于:2016-06-12来源:推酷作者:邹小创点击数: 标签:单元测试
使用这种方式,所有production code都不用专门为testing增加任何多余的代码,同时还能得到依赖注入的其他好处。 Robolectric:解决Android单元测试最大的痛点

  使用这种方式,所有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