1.调研背景
项目面临的问题
- 代码拆分重构后,是否存在问题不好判断,需自测与重新测试。
- 逻辑较复杂的模块,人工代码review不易察觉问题。
- 修改历史bug,需要了解业务、逻辑背景,才能逐步排查问题,比较耗时。
调研的目标
- 针对现有单元测试技术,选择出适合项目使用的单元测试框架,以便能够解决代码拆分、重构后的自测问题。
- 支持性能测试 eg:算法耗时,算法执行次数
- 更快的测试运行速度
- 更全面的测试场景
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:
- 优势:
- 引入了android依赖库, 可在JVM中调用Android相关的类和方法
- 在JVM上运行,不必安装apk,速度较快
- 复写Android核心库(Shadow Classes),扩展更多有用的功能
- 可以对android组件测试 eg: Activity Service Broadcast Receiver
- 可以对资源进行测试 eg: string.xml style等
- 开源的测试工具
- 缺点:
- 不能直接加载使用.so库,so库是linux的动态链接库,而Robolectric运行在JVM上
解决方案如下: - 方案①:不建议在单元测试中加载本地库,在项目中将加载库实现为native方法,单元测试中调用;
- 方案②:借助AndroidJunit来实现对动态库的测试;
- 方案③:动态库一般都是打给特定平台、特定 CPU 架构用的,所以要解决在 Robolectric 下加载运行 so 动态库的问题的思路就是在不同 Robolectric 运行平台下去处理加载不同的动态库。
方案③要求:有so动态库的源码,然后对不同平台macOS 和 Windows打对应的包(macOS需要dylib文件,而windows需要dll)通过对系统识别,实现包的动态加载;
- 不能直接加载使用.so库,so库是linux的动态链接库,而Robolectric运行在JVM上
- 所用到的技术介绍:
-
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测试框架- 优势:
- 能够检测主线程空闲状态时,在适当时候运行测试代码,即不阻塞主线程去同步UI测试.
- 可通过集成或实现接口方式注入IdlingResources来检测异步任务
- 直接获取资源图标进行匹配,克服截图存在分辨率不同的问题
- 图表点击及图片匹配更精准
UI测试三部曲: 定位View -> 操控View ->断言View
-
Espresso有三个重要部分
- ViewMatchers(匹配器): 通过匹配条件来查找指定的UI
- ViewAction(界面行为): 模拟用户操作界面的行为,eg:点击事件
- 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
- 实现逻辑:选择单元测试方案,示例测试代码,收集测试报告
如何写出可进行单元测试的代码
singleton 都是不利于测试的代码方式,会导致需要更多的mock,增加了测试成本.
建议:
1. 方法的书写满足单一职责原则
2. 资源/数据的获取使用依赖注入的方式.
7. 调研总结
为了使得原有代码能够满足单元测试,对项目中,部分模块的代码进行重构.
原始代码重构部分:
- 单一职责: 每个方法只完成一个功能,方便单个功能的测试且满足设计原则。
- 可预测的结果: 可验证的结果 eg:方法有返回值 对数值的改变,状态值的改变,都可通过返回值的方式验证。
- 上下文等此类全局变量,灵活设置,依赖外部传递 eg: setApplication
- 方法的唯一性,可靠性,无副作用: 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 开发者网站
网友评论