对于这个流程,我们做了如下的单元测试:
  CheckoutActivity启动单元测试:通过Robolectric提供的方法,启动一个Activity。验证里面的mCheckoutModel的loadCheckoutData()方法得到了调用,同时参数(订单ID等)是对的。
  CheckoutModel的loadCheckoutData单元测试1:调用CheckoutModel的loadCheckoutData()方法,验证里面的mApi对应的get方法得到了调用,同时参数是对的。
  CheckoutModel的loadCheckoutData单元测试2:mock Api类,指定当它的get方法在收到某些调用的时候,直接调用传入的callback的onSuccess方法,然后调用CheckoutModel的loadCheckoutData()方法,验证Otto bus的post方法得到了调用,并且参数是对的。
  CheckoutModel的loadCheckoutData单元测试3:mock api类,指定当它的get方法在收到某些调用的时候,直接调用传入的callback的onFailure方法,然后调用CheckoutModel的loadCheckoutData()方法,验证Otto bus的post方法得到了调用,并且参数是对的。
  CheckoutActivity的onCheckoutDataLoaded单元测试1:启动一个CheckoutActivity,调用他的onCheckoutDataLoaded(),传入含有正确数据的Event,验证相应的数据view显示出来了
  CheckoutActivity的onCheckoutDataLoaded单元测试2:启动一个CheckoutActivity,调用他的onCheckoutDataLoaded(),传入含有错误信息的Event,验证相应的错误提示view显示出来了。
  这里需要说明的一点是,上面的每一个测试,都是独立进行的,不是说下面的单元测试依赖于上面的。或者说必须先做上面的,再做下面的。
  这部分较为详细的代码放在github上,groupshare这个package里面。
  其他的问题
  以上是我们这边做单元测试用到的技术,以及一个基本流程,下面聊聊其他的几个问题。
  哪些东西需要测试呢?
  1、所有的Model、Presenter/ViewModel、Api、Utils等类的public方法
  2、Data类除了getter、setter、toString、hashCode等一般自动生成的方法之外的逻辑部分
  3、自定义View的功能:比如set data以后,text有没有显示出来等等,简单的交互,比如click事件,负责的交互一般不测,比如touch、滑动事件等等。
  4、Activity的主要功能:比如view是不是存在、显示数据、错误信息、简单的点击事件等。比较复杂的用户交互比如onTouch,以及view的样式、位置等等可以不测。因为不好测。
  CI和code coverage: Jacoco
  要把单元测试正式化,CI是非常重要的一步,我们有一个运行Jenkins的CI server,每次开发者push代码到master branch的时候,会运行一次单元测试的gradle task,同时使用Jacoco做code coverage。
  这里有个坑要特别注意,那是项目里面的gradle Jacoco插件和Jenkins的Jacoco插件的兼容性问题。我们用的gradle Jacoco插件是7.1,更高版本的好像有问题。然后对应的Jenkins的Jacoco插件需要1.0.19或更低版本的,更高版本的jenkins plugin不支持低版本的gradle Jacoco项目版本。实际上,这点在Jenkins的Jacoco插件首页有说明:

  但是我当时没注意,所以覆盖率数据一直出不来,折腾了好一会,后还是在同事的帮助下找到问题了。
  遇到的坑,以及好的practice建议
  接下来讲讲我们遇到的一些坑,以及一些好的practice建议。
  1. Native libary
  无论是纯JUnit还是Robolectric,都不支持load native library,会报UnsatisfiedLinkError的错。所以如果你的被测代码里面用到了native lib,那么可能需要给System.loadLibrary加上try catch。
  如果是被测代码用到的第三方lib,而里面用到了native lib的话,一般有两种解决办法,一种是将用到native lib的第三方类外面自己在包一层,然后在测试的情况下mock掉。第二种是用Robolectric,给那个类创建一个shadow class。
  第一种方法的好处是可以在测试的时候随时改变这个类的返回值或行为,缺点是需要另外创建一个wrapper类,会有点繁琐。第二种方式不能随时改变这个类的行为,但是写起来非常简单。所以,看自己的需要,选择相应的方法。
  这两种方法,也是解决static method, final class/method不能mock的主要方式。
  2. 尽量写出易于测试的代码
  static method、直接new object、singleton、Global state等等这些都是一些不利于测试的代码方式,应该尽量避免,用依赖注入来代替这些方式。
  3. 不要重复你的unit test
  比如说你使用了一个builder模式来创建了一个类,这个builder有一个validator,来validate一些参数情况。那么这种情况,builder跟validator分开测,用各种正确的错误的参数情况去测试validator,然后测builder的时候,不用遍历各种有效的跟无效的参数去测试了。
  因为如果这样的话,到时候Validator的逻辑改了,那么针对Validator的测试跟针对Builder的测试都要修改,这个其实是重复的。这里只需要测试这个builder里面有一个Validator好了。
  4. 公共的单元测试library
  如果你们公司也是组件化开发的话,抽出一个公共的单元测试类库来做单元测试,里面可以放一些公共的helper、utils、rules等等,这个可以极大的提高写单元测试的速度。
  5. 把安卓里面的“纯java”代码copy一份到自己的项目里面
  安卓里面有些类其实跟安卓没太大关系的,比如说TextUtils、Color等等,这些类完全可以把代码copy出来,放到自己的项目里面,然后其他地方用这个类,这样也能部分摆脱android的依赖,使用JUnit而不是Robolectric,提高运行test的速度。
  6. 充分发挥JUnit Rule的作用
  JUnit Rule是个很强大的工具,然而知道的人却不多。它的基本作用是,让你在执行某个测试方法前后,可以做一些事情。如果你的好几个测试类里面有很多的共同的setup、teardown工作,你可能会倾向于使用继承,结合@Before、@After来减少duplication,这里更建议大家使用JUnit Rule来实现这个目的,而不是用继承,这样可以有更大的灵活性。
  比如,为了方便测试Activity的method,我们有一个ActivityRule,在跑一个测试方法之会启动target Activity,然后跑完以后自动finish这个activity。