Android单元测试调研

1.调研背景

项目面临的问题

  1. 代码拆分重构后,是否存在问题不好判断,需自测与重新测试。
  2. 逻辑较复杂的模块,人工代码review不易察觉问题。
  3. 修改历史bug,需要了解业务、逻辑背景,才能逐步排查问题,比较耗时。

调研的目标

  1. 针对现有单元测试技术,选择出适合项目使用的单元测试框架,以便能够解决代码拆分、重构后的自测问题。
  2. 支持性能测试 eg:算法耗时,算法执行次数
  3. 更快的测试运行速度
  4. 更全面的测试场景

2. 拟调研方案(集)

Java单元测试框架

  • Junit
  • Mockito
  • Powermockito

Android单元测试框架:

  • AndroidJUnitRunner
  • Robolectric

AndroidUI测试框架:

  • Espresso

3. 比对维度设定及说明

  • 运行平台:运行在JVM 或 Android设备上
  • 运行耗时:是否能更快运行测试代码,快速实现小粒度的单元测试
  • 版本:是否可覆盖大多数版本
  • 是否开源:测试框架是否开源
  • 环境配置:是接入成本的一部分,环境配置是否方便;
  • 功能支持:是否能够覆盖更多的测试场景

4. 调研过程

各个方案在预定维度上面的表现

Java单元测试.png
Android单元测试.png

各个方案总结

  • 主要从运行平台、功能支持、运行耗时等维度进行对比:

  • Junit为java单元测试框架,不依赖Android框架,虽然可借助Mockito隔离依赖Android,但编写维护模拟代码是有成本的,并且无法支持android特有的组件、生命周期等。所以,排除此框架;

  • AndroidJUnitRunner是Google官方的android单元测试框架之一,需要运行在Android真机或模拟器环境。需要安装2个apk,运行速度比直接运行app还要慢。不满足我们快速单元测试的需求,排除;

  • Robolectric引入了Android依赖库, 可在JVM中调用Android相关的类和方法。运行速度介于二者之间(约十几s) 对比以上两者都有明显优势。版本兼容方面:使用Robolectric4.0版本以上兼容最高版本API28 要求Android studio>=3.2以上 目前开发中studio版本为3.3 满足此要求;

  • Robolectric

    1. 优势:
      • 引入了android依赖库, 可在JVM中调用Android相关的类和方法
      • 在JVM上运行,不必安装apk,速度较快
      • 复写Android核心库(Shadow Classes),扩展更多有用的功能
      • 可以对android组件测试 eg: Activity Service Broadcast Receiver
      • 可以对资源进行测试 eg: string.xml style等
      • 开源的测试工具
    2. 缺点:
      • 不能直接加载使用.so库,so库是linux的动态链接库,而Robolectric运行在JVM上
        解决方案如下:
      • 方案①:不建议在单元测试中加载本地库,在项目中将加载库实现为native方法,单元测试中调用;
      • 方案②:借助AndroidJunit来实现对动态库的测试;
      • 方案③:动态库一般都是打给特定平台、特定 CPU 架构用的,所以要解决在 Robolectric 下加载运行 so 动态库的问题的思路就是在不同 Robolectric 运行平台下去处理加载不同的动态库。
        方案③要求:有so动态库的源码,然后对不同平台macOS 和 Windows打对应的包(macOS需要dylib文件,而windows需要dll)通过对系统识别,实现包的动态加载;
    3. 所用到的技术介绍:
      • Robolectric的Shadow Classes:
        Robolectric有很多Shadow类来修改或拓展Android原本的类,每一次执行Android类时,Robolectric确保Shadow类先执行。
        作用:覆盖Android sdk行为,确保通过 ClassLoader加载Robolectric提供的android-all.jar使得在JVM上运行Android可行。
        Shadow提供了更多的扩展方法,并且相关依赖满足最小依赖的设计原则,被切分为多个模块:


        Shadow.png
UI测试:
  • Espresso的介绍
    Espresso是谷歌推荐的UI测试框架

    • 优势:
    1. 能够检测主线程空闲状态时,在适当时候运行测试代码,即不阻塞主线程去同步UI测试.
    2. 可通过集成或实现接口方式注入IdlingResources来检测异步任务
    3. 直接获取资源图标进行匹配,克服截图存在分辨率不同的问题
    4. 图表点击及图片匹配更精准
      UI测试三部曲: 定位View -> 操控View ->断言View
  • Espresso有三个重要部分

    1. ViewMatchers(匹配器): 通过匹配条件来查找指定的UI
    2. ViewAction(界面行为): 模拟用户操作界面的行为,eg:点击事件
    3. ViewAssertions(界面判断):对模拟行为操作的View进行变换和结果验证
  • 异步方法测试存在的问题:
    测试代码是同步的,测试代码已经执行完毕,而异步任务还未返回,所以需要测试代码支持异步。

  • Espresso特点:
    Espresso测试有个很强大之处就是它在多个测试操作中是线程安全的,它会等待当前进程的消息队列中的UI事件,并且在任何一个测试操作中会等待其中的AsyncTask结束才会执行下一个测试。
    即如果代码中通过AsyncTask或者AsyncTaskCompat方式来执行异步任务,无需额外处理,交由Espresso处理,它会帮助我们执行异步方法后,再执行测试代码的断言处理.

  • 异步任务的实现方式非AsyncTask:
    方案①:
    Espresso提供了IdlingResource接口 Espresso会等待AsyncTask和IdlingResource执行完毕后才会执行我们写的测试代码 所以实现IdlingResource接口即可以实现测试异步方法
    方案②:
    由于Espresso框架本身会在AsyncTask运行期间,阻塞下一条测试断言,那么可以将异步任务线程切换到AsyncTask所在的线程池执行 eg:通过RxJava实现

Mockito模拟技术:

模拟对象,模拟接口/方法的行为,以实现复杂功能的解耦,从而得到响应值。
使用场景: 在测试过程中,某些不易构造或不易获取的对象,模拟一个虚拟的对象,以便有效的执行测试方法.
举例说明:

public interface IMathUtils {

    /**
     * 求绝对值
     * @param num
     * @return
     */
    public int abs(int num);
}

import org.junit.Assert;
import org.junit.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class IMathUtilsTest {

    @Test
    public void abs() {
        IMathUtils mathUtils =  mock(IMathUtils.class);\
        when(mathUtils.abs(-1)).thenReturn(1);
        int abs = mathUtils.abs(-1);
        Assert.assertEquals(abs,1);
    }
}

IMathUtils是一个接口,未被实现,使用Mockito框架后,可以模拟出实例对象,并调用相关方法.但mock对象调用任何方法,并不会被实际执行,只是模拟方法行为,并模拟返回数据.
注意: Mockito并不会为真实的对象代理函数调用,实际上它会复制真实对象. 所以: Mock声明的对象,对函数的调用均执行mock(即虚假函数),不执行真正部分。
原理:Mockito底层用了CGLib(github/cglib)做动态代理;

CGLib:功能强大,高性能的代码生成包,能够为没有实现接口的类提供代理。
CGLib原理:动态生成一个要代理类的子类,子类重写要代理的类的所有非final的方法。在子类中拦截所有父类方法的调用。它比使用java反射的JDK动态代理要快。
CGLIB底层:使用字节码处理框架ASM,来转换字节码并生成新的类。Java的动态代理制能支持接口的形式,而使用ASM能够扩展到类的代理。
CGLIB缺点:对于final方法,无法进行代理。
在java中,可以使用java的动态代理创建代理,但当代理的类没有实现接口或者为了更好的性能,可以用CGLib的方式。

Powermockito

对Mockito的扩展: 支持mock匿名类、final类、static方法、private方法

5. 调研结论

  • 由于 Robolectric + Mockito 这两个测试框架都为开源框架,该方案扩展性强,运行速度较快, 所以最终采用Robolectric + Mockito的方案进行单元测试

6. 落地方案

  • 一期方案:
    • 实现排期:2019.07.26-2019.08.09
    • 实现逻辑:选择单元测试方案,示例测试代码,收集测试报告
      如何写出可进行单元测试的代码
    问题: 单元测试最大的痛点是代码的耦合,eg: 直接持有三方库的引用,不合理的跨层调用等,还有 new object
    singleton 都是不利于测试的代码方式,会导致需要更多的mock,增加了测试成本.
    建议:
    1. 方法的书写满足单一职责原则
    2. 资源/数据的获取使用依赖注入的方式.

7. 调研总结

为了使得原有代码能够满足单元测试,对项目中,部分模块的代码进行重构.

原始代码重构部分:
  1. 单一职责: 每个方法只完成一个功能,方便单个功能的测试且满足设计原则。
  2. 可预测的结果: 可验证的结果 eg:方法有返回值 对数值的改变,状态值的改变,都可通过返回值的方式验证。
  3. 上下文等此类全局变量,灵活设置,依赖外部传递 eg: setApplication
  4. 方法的唯一性,可靠性,无副作用: eg:纯函数
单元测试编写部分:

1.单元测试的边界: 跨模块调用,例如存在无法获取的中间模块,可以通过mock方式隔离。只验证相关模块对应的能力,其他模块的单元测试,由其他模块自行提供。
2.本地文件的读取验证: 索引项目本地路径,通过java方式读取,来验证。
3.静态方法: Mockito无法mock的静态方法,可封装成非静态方法再mock 或者使用Shadow来模拟。
4.依赖隔离: 阻塞测试的中间环节,都可以通过mock跳过隔离 eg: application eg:XXXExportedProxy 的function
5.交互相关若阻塞测试: 模拟交互结果,直接测试逻辑 eg:执行js方法 回调前端方法,获取图片资源

参考文档:

  • Junit: Junit官网
  • AndroidJUnitRunner: AndroidJUnitRunner 开发者网站
  • Mockito: Mockito官网
  • Powermockito:Powermockito官网
  • Robolectric:Robolectric官网
  • Espresso: Espresso 开发者网站

你可能感兴趣的:(Android单元测试调研)