美文网首页
Android单元测试调研

Android单元测试调研

作者: Shmily鱼 | 来源:发表于2022-05-31 11:41 被阅读0次

    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方法 回调前端方法,获取图片资源

    参考文档:

    相关文章

      网友评论

          本文标题:Android单元测试调研

          本文链接:https://www.haomeiwen.com/subject/zbtqpktx.html