单元测试小结

最近一段时间工作重点在单元测试上,在这里总结一些两个月来对android单元测试的收获。

单元测试,在android应用这一层主要还是针对类的方法成员或者一个基本功能单元。大体上可以分三步:模拟测试环境;运行测试目标;检查运行结果。

模拟测试环境,实际上就是做好针对测试目标代码的运行准备。在junit框架中,setUp 和 tearDown 回调,就是干这个的。前者运行于每个测试用例之前,在这里实现用例中需要的环境;后者运行于每个用例之后,做一些关闭、释放的操作,算是扫尾。我认为在这个环节中实际上是一个解依赖的过程,需要把影响到测试目标运行的依赖解除掉或者模拟一个假的依赖来供给它运行,目的只有一个——在测试工程中让目标代码可控。

运行测试目标,当然就是跑代码。但是这里应该还是有讲究的,比如针对一个功能函数,那就要考虑不同的参数与相应的运行结果,包括运行中可能发生的异常。从参数上,大概分三种,正常参数、边界参数和异常参数,用例的输入的丰富程度,在某些方面决定了目标代码的覆盖程度,也就是整个测试用例的有效性。

检查运行结果,就是一个期望值与运行值的对比。Android junit把这部分功能函数都封得差不多了,各种assertXXX用起来还是挺方便的。实际上断言运行结果这部分还是比较考验目标代码的可测试性的,我的理解是,一个函数或功能是否可测、易测,除了它运行的环境是否容易模拟和注入以外,还有一点就是它运行产生的影响是否明确、是否容易获取。比如说我最喜欢看到的纯功能的静态函数,这种货色一般就是多参然后返回值就是结果,以类名调用用例写起来相当轻松。

针对已有代码建立测试工程时,就会发现需要考虑到要在对目标代码封装性破坏最小的前提下来实现测试。久而久之很自然的就认为在代码设计或重构的过程中,需要在可测试性这方面要多加注意。考虑在前面总比后面在写用例时强行添加注入方法、重新抽象依赖接口要来的实在,设计时照顾到这一点后面就会省不少事儿。下面几点算是在这些方面一些方法的总结:

  1. 设计时需要考虑将硬件相关的功能(如磁盘剩余容量、时间、广播发送、网络状态获取等)抽象成接口;
    测试代码的运行要尽可能少的依赖硬件,因为我们的目标是检查另外一段代码的逻辑、运行结果是否正确,成本自然越小越好,并且部分硬件相关的功能目前还没有好的办法去检测。所以好的处理方式是将硬件相关的部分功能抽象在一起,形成一个SystemFacade接口类,在工程代码中使用真正的系统功能实现RealSystemFacade;而在测试工程中,我们可以再模拟一个MockSystemFacade出来通过注入或者继承的方式替换掉原本的真正的实现类,万事大吉,这一招是从android 下载部分代码中学到的。举个例子,广播,广播的发送过程是由系统控制的,在应用这个层面上能做什么呢?只能知道发送了什么、知道收到了什么之后的处理过程。所以在测试过程中就要丢掉发送的过程,我们保证发送的Intent都是正确的就足够了。这时就可以把发广播这个动作抽象到前面说的SystemFacade中,然后在模拟的facade中只需要记录下发送了哪些Intent,之后取出来断言发送的是否正确就可以了。《重构》一书中的“接缝”的概念与此大致相同。

  2. 继承与注入;
    测试一个实现好的工程,很多时候都要在一定程度上破坏目标工程的封装,也算是不得已而为之吧。目前我用的比较带劲儿的就是继承与注入:继承,目标类中存在一些碍于运行的属性或方法,需要替换成我们想要的数据或返回值,这时可以在目标代码中提升这个属性或方法的权限到protected以上。然后在测试工程中新建一个测试目标类继承被测试类,以override的方式替换掉碍眼的东西;注入,就需要在目标代码中开一个set接口,然后在测试工程中把我们模拟好的类set进取,这一招一般与前面的抽象接口的方法配合使用。

  3. 代码设计时需要考虑函数的断言方法,提高可测试性(纯计算或逻辑的功能函数可以考虑抽象成静态方法);
    就这一点而言,可以说函数设计,功能要单一、长短要适中,和可读性的要求基本是一致的,不一致的地方是,可测试性好的代码则还要求能够容易地找到代码带来的改变,从而能够容易的断言出函数是否运行的符合期望。和之前提到的测试用例编写的第三步是一回事。

  4. 采取测试工程测试代码与目标代码同包名的方法,尽量降低对目标代码封装的破坏;
    我没有深入的了解整个测试框架的编译和运行过程,但是从实际效果来看很有可能两部分代码最终是打包到一起编译的。测试工程的包名和目标工程的包名可以相同,并且实际编译效果也与同包名下类之间的相同。这样就在一定程度上能够避免对目标工程代码的修改,破坏原来的封装了。

  5. mock,擅用已有的成熟的mock库;
    mock是单元测试的常用手段,比如模拟符合测试需要的网络响应、收集网络请求来提供断言依据等。目前使用到两种,一种是针对http协议的 mockwebserver 库;另外一种是针对ftp协议的 MockFtpServer 库。用起来也比较顺手,非常方便。

暂时就这么多,不断总结,不断进步。

感谢您赏个荷包蛋~