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

参考文档:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容