背景
很多团队都是通过测试这一流程来作为代码高质量上线的最后一道关卡,所以保证测试这一流程不出问题是非常重要的。
因此为了提高代码质量,通常有以下几种方案:
- 通过单测,来cover部分代码逻辑的边界
- 通过代码覆盖率,让测试团队的黑盒测试尽可能的覆盖完大部分分支
- 通过自动化测试,把部分人工验证的场景交给机器验证
当然,就算把上边的几种方案都做了,也不能保证线上不出问题,不过较大程度上的降低线上出现问题的场景,收益还是比较大的。
本文会针对代码覆盖率这一场景进行分析。在android的代码覆盖率使用中,使用比较多的还是jacoco。在android的整个工具链中,也已经内置了jacoco了。我们可以不需要引入其他库也能够使用jacoco。
jacoco使用
在Android中使用jacoco进行代码覆盖率比较简单,按照下面几个步骤即可开启并展示结果。
开启jacoco插件
1plugins {
2 id 'com.android.application'
3 id 'kotlin-android'
4 id 'jacoco'
5}
6
7jacoco {
8 toolVersion = "0.8.5"
9}
jacoco在androidStudio已经内置了。在androidStudio中可以直接开启jacoco插件。
可以通过jacoco的Extension设置指定的的jacoco版本。
开启打包插桩开关
1 buildTypes {
2 release {
3 minifyEnabled false
4 testCoverageEnabled = true
5 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
6 }
7 debug {
8 testCoverageEnabled = true
9 }
10 }
需要在BuildTypes中,针对不同的打包类型,通过 testCoverageEnabled 开启代码插桩。
保存代码覆盖结果
通过编译期间的代码插桩,运行时会实时记录代码运行情况。需要我们在我们自定义的时机保留代码覆盖结果。支持同一个文件追加写入。
1object JacocoUtil {
2 val ecFile = File(Environment.getExternalStorageDirectory(), "/coverage.ec")
3 fun generateEcFile() {
4
5 val agent = Class.forName("org.jacoco.agent.rt.RT")
6 .getMethod("getAgent")
7 .invoke(null)
8 writeBytes2File(ecFile.absolutePath, agent.javaClass.getMethod("getExecutionData",
9 Boolean::class.javaPrimitiveType).invoke(agent, false) as ByteArray)
10 }
11 }
通过反射获取jacoco的Agent实例,再通过反射获取出来代码覆盖结果的字节流,写入到文件中。
1public byte[] getExecutionData(final boolean reset) {
2 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
3 try {
4 final ExecutionDataWriter writer = new ExecutionDataWriter(buffer);
5 data.collect(writer, writer, reset);
6 } catch (final IOException e) {}
7 return buffer.toByteArray();
8 }
从手机中dump指定覆盖率文件,放在指定文件夹
1adb pull sdcard/coverage.ec xxx/MyTest/app/build/ecf
通过保留文件,解析代码覆盖率报告
在gradle文件中,增加处理任务。
1task jacocoTestReport(type: JacocoReport) {
2 group = "JacocoReport"
3 description = "Generate Jacoco coverage reports after running tests."
4 reports {
5 html.enabled = true
6 }
7 classDirectories.from = files(files(coverageClassDirs).files.collect {
8 fileTree(dir: "$rootDir" + it)
9 })
10 sourceDirectories.from = files(coverageSourceDirs)
11 executionData.from = files("$buildDir/ecf/coverage.ec")
12
13 doFirst {
14 coverageClassDirs.each { filePath ->
15 println("$rootDir" + filePath)
16 new File("$rootDir" + filePath).eachFileRecurse { file ->
17 if (file.name.contains('$$')) {
18 file.renameTo(file.path.replace('$$', '$'))
19 }
20 }
21 }
22 }
23}
查看报告
在执行完生成报告Task之后,可以看到在build目录下会多生成一个reports文件夹,生成的报告就在里面。
查看对应的覆盖结果如下:
各个类覆盖详情:
列表展示
单个类的覆盖详情:
jacoco原理
看完了jacoco的简单使用,我们再来看看jacoco的实现
构建方式
jacoco整体是使用maven的方式去构建的。maven构建的方式是java中通用的构建方式。
maven构建相对于android中gradle构建区别还是比较大的。最主要的不同点是在构建配置
配置上:
Gradle基于groovy语言和DSL语法提供了简明、灵活、可读性强的配置方式
maven使用xml文件格式进行配置,较为繁琐
可扩展性:
gradle扩展任何语言的构建,maven不行。
构建性能:
gradle支持增量构建
gradle支持构建缓存
gradle支持守护进程
所以gradle的构建性能要优于maven。
插件开发
在阅读代码插桩入口时,发现类上边都会打上一个特定的注解Mojo
1@Mojo(name = "instrument", defaultPhase = LifecyclePhase.PROCESS_CLASSES, threadSafe = true)
2public class InstrumentMojo extends AbstractJacocoMojo {}
Maven plain Old Java Object ,mojo是基于maven的插件开发的注解。每一个mojo对象都是一个执行目标。
类似于gradle中的gradle-plugin。一个mojo就对应着一个java类,在整体jar包编译时,会执行配置的插件。
代码插入方式
jacoco记录代码覆盖率完全依赖于对原始代码的插桩,需要在原始代码中插入探针,通过运行时记录探针执行来计算代码覆盖来。
插入代码有两套实现
动态方式:在Jvm加载class过程中,动态的去修改class
离线模式:在编译class的阶段,对原始class进行修改,生成类已经带有全量的插入代码
动态方式之javaAgent
利用JVM提供的
InstrumentAPI
来更改加载的到JVM的现有字节码。
JavaAgent也有两种方式修改字节码
静态修改:在加载jar包之前修改字节码。静态加载会调用到premain方法。
java -javaagent:agent.jar -jar xxx.jar
比如jacoco就使用的静态修改的方式。可以查看其PreMain的premain方法
1public static void premain(final String options, final Instrumentation inst)
2 throws Exception {
final AgentOptions agentOptions = n