TA的每日心情 | 擦汗 7 小时前 |
---|
签到天数: 947 天 连续签到: 6 天 [LV.10]测试总司令
|
当业务快速发展,新业务不断出现,开发同学粗心的情况下,难免会出现少测漏测的情况,如何保证新增代码有足够的[url=]测试[/url]覆盖率?当一段正常的代码,开发却修改了,测试人员没有测试其功能,如果保证能够发现?
所以代码覆盖测试是有必要的,代码覆盖只能保证这行代码执行了,不能保证其是否正确。寻找相关工具,发现最接近的是jacoco。jacoco 接入也比较简单,在安卓上用的offline 模式,不过jacoco 默认是全部插入探针代码,所以需要对其改造,只对增量代码插入探针。
大致流程
需求开发流程:项目管理是git,master 分支是线上分支,开发人员在开发某个需求时,会从master 拉取新分支开发,测试完成,封板上线后,会把分支合到master 上。确保master 永远是线上代码。
测试流程:开发人员在开发时,是开发包,开发完成会打测试包给测试人员,测试人员反馈问题,开发修改代码,再次打包,这一过程可能会重复多次。测试通过,告知开发打正式包。最终上线正式包。我们对开发包称为debug 包,测试包称为beta包,正式包称为release 包。buildType中对应三种打包方式。
- buildTypes {
- debug {...
- }
- release {...
- }
- beta {...
- }
- }
复制代码 其中debug 包是开发人员直接安装运行,beta与release是通过 jenkins 连打包机打出来供测试人员下载。
所以我们在插入探针代码时,只需要对beta 包插入,然后测试人员下载,手动测试,本地生成数据,上传数据给服务器。供后续生成报告时使用,使用开关如下:
- jacocoCoverageConfig {
- jacocoEnable isBeta()
- ....
- }
- def isBeta() {
- def taskNames = gradle.startParameter.taskNames
- for (tn in taskNames) {
- if (tn == "assembleBeta" || tn == "ttpPackageBeta") {
- return true
- }
- }
- return false
- }
复制代码 在打release 包时,调用生成报告的任务。查看本次增量代码的覆盖率报告,输出报告到apk目录,供开发人员查看,由开发人员判断这个覆盖率是否合理。当然你也可以在低于百分之多少时抛出异常,中断打包。
框架的整体流程如下:
首先分为三块:
1、编译时:这里说的是开关为打开的情况,编译时主要是获取两个分支的差异方法集合,然后调用jacoco提供的方法,对差异方法代码插入探针。
2、App 运行时:测试人员在运行带有探针的包,会把探针运行数据.ec 保存在本地,下次再打开app时上传上次数据。
3、生成报告:打正式包时,下载此项目版本所有的覆盖数据,和编译时一样,获取两分支差异方法集合。调用jacoco方法,生成最终的差异方法报告。
下面分别对各个流程中一些技术难点说明。
一、编译时
编译时是通过gradle TransForm实现的,TransForm可以对字节码进行修改。主要过程分为三大步,class git 管理、获取差异方法、对diff方法插入探针。
大致代码如下:
- JacocoTransform.groovy
- @Override
- void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
- ……
- if (!dirInputs.isEmpty() || !jarInputs.isEmpty()) {
- if (jacocoExtension.jacocoEnable) {
- //copy class到 app/classes
- copy(transformInvocation, dirInputs, jarInputs, jacocoExtension.includes)
- //提交classes 到git
- gitPush(jacocoExtension.gitPushShell, "jacoco auto commit")
- //获取差异方法集
- BranchDiffTask branchDiffTask = project.tasks.findByName('generateReport')
- branchDiffTask.pullDiffClasses()
- }
- //对diff方法插入探针
- inject(transformInvocation, dirInputs, jarInputs, jacocoExtension.includes)
- }
- }
复制代码 1.1 class git 管理
首先项目中是有java 和kotlin 源码。如果解析源码文件,需要对两种语言适配。 而无论是java 还是kotlin ,编译完都是 .class,解析class 可以通过 ASM 。jacoco 也需要ASM,所以我们需要保存源码对应的 class 文件,当然也不能全部保存,只保存自己的包名的,例如前辍 com.ttpc 。一些第三方的源码,我们认为它是稳定的,没问题的,也就没必要对其进行覆盖测试,把编译后的class copy到项目的app 目录下,与src 同级,例:
这些 class 也是需要通过git 管理的。然后自动执行git add、commit、push 命令,提交到git 服务器。因为通过 git 可以获得两个服务器分支差异的文件名。
1.2、获取两个分支差异方法集
其中编译时与生成报告时都需要获取 "两个分支差异方法集"。其中一个分支就是当前开发分支,一个是master 分支(可配置)。差异方法定义无论是新增方法,还是修改了方法,那怕修改一行代码,都算是差异方法,那个整个方法都要覆盖到。
以dev_3 为开发当前分支,master 为稳定分支举例。当前分支通过 git name-rev --name-only HEAD 获取 。
1.2.1、获取差异文件名集
通过 git 可以获得两个分支差异的文件名。
git diff origin/dev_3 origin/master --name-only
输出如下:
通过 \n 分隔,得到差异文件名集合。通过后辍过滤非 .class 与非包名文件。
ok,现在得到两分支差异class文件名,但是我们需要精确到差异方法。
1.2.2、copy 两分支差异文件
接下来,切换到master 分支,把所有class copy 到一个临时目录。
再切回 当前dev_3分支,把所有class copy 到临时目录。(临时目录和项目同级,为了不影响项目)
删除那些不在 差异文件名集合 的文件,得到差异文件集。
切换分支+copy 如下:注意是强制切换,会导致工作区丢失。
- #!/bin/sh
- gitBran=$1 # 要切换的分支
- workDir=$2 #当前目录
- outDir=$3 # copy 输出目录
- git checkout -b $gitBran origin/$gitBran
- git checkout -f $gitBran
- git pull
- cp -r "${workDir}/app/classes" $outDir
复制代码
1.2.3、生成差异方法集
对两个分支目录的所有class,使用ASM读取class,访问方法,收集方法信息,关键代码如下:
- public class DiffClassVisitor extends ClassVisitor {
- ……
- @Override
- public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
- MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
- final MethodInfo methodInfo = new MethodInfo();
- methodInfo.className = className;
- methodInfo.methodName = name;
- methodInfo.desc = desc;
- methodInfo.signature = signature;
- methodInfo.exceptions = exceptions;
- mv = new MethodVisitor(Opcodes.ASM5, mv) {
- StringBuilder builder = new StringBuilder();
- //访问方法一个参数
- @Override
- public void visitParameter(String name, int access) {
- builder.append(name);
- builder.append(access);
- super.visitParameter(name, access);
- }
-
- //访问方法一个注解
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- builder.append(desc);
- builder.append(visible);
- return super.visitAnnotation(desc, visible);
- }
- //访问ldc指令,也就是访问常量池索引
- //与方法体有关,需要参与md5
- @Override
- public void visitLdcInsn(Object cst) {
- //资源id 每次编译都会变,所以不参与 0x7f010008
- if (!(cst instanceof Integer) || !isResourceId((Integer)cst)) {
- builder.append(cst.toString());
- }
- super.visitLdcInsn(cst);
- }
- ……
- //方法访问结束
- @Override
- public void visitEnd() {
- String md5 = Util.MD5(builder.toString());
- methodInfo.md5 = md5;
- DiffAnalyzer.getInstance().addMethodInfo(methodInfo, type);
- super.visitEnd();
- }
- }
复制代码 其中MethodVisitor 有很多 visitXxx方法,都是方法的基本信息与一些指令。然后对其md5 ,得到方法的md5签名。通过classsName,methodName,desc来定位同一方法,然后比较其md5是否一致。一致则代表未修改过代码。这里要注意的是visitLdcInsn 访问常量池指令,因为每次编译时,资源id都会不一致,所以要过滤掉资源id。
当所有的class 访问结束,通过两个分支方法集,得到差异方法集。
- public void diff() {
- if (!currentList.isEmpty() && !branchList.isEmpty()) {
- for (MethodInfo cMethodInfo : currentList) {
- boolean findInBranch = false;
- for (MethodInfo bMethodInfo : branchList) {
- if (cMethodInfo.className.equals(bMethodInfo.className)
- && cMethodInfo.methodName.equals(bMethodInfo.methodName)
- && cMethodInfo.desc.equals(bMethodInfo.desc)) {
- if (!cMethodInfo.md5.equals(bMethodInfo.md5)) {
- diffList.add(cMethodInfo);
- }
- findInBranch = true;
- break;
- }
- }
- if (!findInBranch) {
- diffList.add(cMethodInfo);
- }
- diffClass.add(cMethodInfo.className);
- }
- }
- }
复制代码 1.2.4、插入探针代码
调用jacoco 的instrument ,把插入探针后的字节码写入文件。
- ClassInjector.class
- @Override
- void processClass(File fileIn, File fileOut) throws IOException {
- if (shouldIncludeClass(fileIn)) {
- InputStream is = null;
- OutputStream os = null;
- try {
- is = new BufferedInputStream(new FileInputStream(fileIn));
- os = new BufferedOutputStream(new FileOutputStream(fileOut));
- // For instrumentation and runtime we need a IRuntime instance
- // to collect execution data:
- // The Instrumenter creates a modified version of our test target class
- // that contains additional probes for execution data recording:
- final Instrumenter instr = new Instrumenter(new OfflineInstrumentationAccessGenerator());
- final byte[] instrumented = instr.instrument(is, fileIn.getName());
- os.write(instrumented);
- } finally {
- closeQuietly(os);
- closeQuietly(is);
- }
- } else {
- FileUtils.copyFile(fileIn, fileOut);
- }
- }
复制代码
|
|