android代码覆盖率jacoco实践

简记代码覆盖率工具jacoco(Java Code Coverage)在android项目中的实践。

app目录里面的build.gradle文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apply plugin: 'jacoco'
android {
buildTypes {
debug {
testCoverageEnabled = true
}
}
}
// 用于从.ec文件生成coverage报告
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports on the build."
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes",
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/BuildConfig.*',
'**/Manifest*.*']
)
def coverageSourceDirs = [
"src/main/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = fileTree(dir: './build/outputs', include: '**/*.ec')
reports {
xml.enabled = true
html.enabled = true
}
}

AndroidManifest.xml:

1
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

启动第一个activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d("tttttest", "onCreate: MainActivity");
String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (!file.exists()) {
try {
file.createNewFile();
Log.d("tttttest", "Main create file /mnt/sdcard/coverage.ec ");
} catch (IOException e) {
e.printStackTrace();
}
}
}

第一个activity,可以在AndroidManifest.xml中查找android.intent.action.MAIN得到,也可以启动app时由adb logcat | grep ActivityManager得到。

按机身back键退出app时必经的activiy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onDestroy() {
OutputStream out = null;
try {
out = new FileOutputStream("/mnt/sdcard/coverage.ec", true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
} catch (Exception e) {
Log.d(TAG, e.toString(), e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Log.d("tttttest", "XXXActivity onDestroy");
}

点back键退出时检查日志,看onDestroy()方法是否被调用了。out = new FileOutputStream("/mnt/sdcard/coverage.ec", true); 写成true即追加而不是覆盖。

一般实际项目中,这些activity通常会继承一个上层activity(一些通用方法的封装), onCreate()和onDestroy()中增加的coverage相关方法也可以写在这个父activity类中。

执行完测试后,adb pull /mnt/sdcard/coverage.ec到app/build/outputs里面(该目录在task jacocoTestReport配置),然后项目根目录下运行./gradlew jacocoTestReport,最后在app/build/reports中查看报告即可。

错误解决

错误1:
执行./gradlew jacocoTestReport报错:

[jacoco:report] Classes in bundle ‘Code Coverage Report’ do no match with execution data. For report generation the same class files must be used as at runtime.
[jacoco:report] Execution data for class xxxxx does not match.
[jacoco:report] Execution data for class yyyyy does not match.

原因,编译apk的Java版本和生成Jacoco报告的Java版本不一致(都是Java8,但小版本不一致)。android studio默认的JDK不是电脑系统的而是自带的/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin。(在项目上右键Open Module Settings可以看到)这里有两篇文章解释的比较详细:文章一文章二

错误2:
有时候遇到其它莫名错误,可以用Android Studio的Build - Project Clean一下,或者Invalidated Caches / Restart...

错误3:
./gradlew jacocoTestReport生成报告时:

Caused by: java.lang.IllegalStateException: Can’t add different class with same name:

存在同名class,(intermediates/classes下有debug和release两个文件夹),删除release文件夹或者classDirectories写到debug,${project.buildDir}/intermediates/classes/debug

报告合并

多个.ec文件直接扔到app/build/outputs文件夹中(该目录在task jacocoTestReport配置)即可,会自动合并报告。

自动插桩

可以把变更打成svn patch,

1
svn diff > ../coverage.patch

svn 重新拉取代码后,导入 patch

1
svn patch ../coverage.patch

git也一样,命令略有差异。

todo

1,做server,可以接收文件,手机端退出时自动把ec报告发送至server。
2,2017年GITC全球互联网技术大会上,听了饿了么邱化峰的报告,他们代码覆盖率做得比较深入(以下摘自邱化峰ppt):

  • 最少用例数
    覆盖所有情况使用的最少用例数,在系统软件和军事软 件有75%的在使用,将圈复杂度维持在10以下有很多实 际的和经济的理由,低于10的软件是非常简单且很容易 跟踪的,
  • 圈复杂度-降低圈复杂度的重构技术
    代码复杂度的衡量标准,程序的可能错误和高的圈复杂度有着很大关系,圈复杂度可以成为编码及重构的重要参考指标。参考 《代码大全》 《重构 改善既有代码的设计》。
    • 提取函数— 将独立业务活模块代码单独封装为函数
    • 算法替换—复杂的算法可能会导致Bug,满足功能的前提下,使用简单的算法
    • 解条件式—复杂的条件表达式封装为函数
    • 合并条件式—将一系列得到相同结果的条件表达式合并
    • 查询函数和修改函数分离—-单一职责原则,强调复用性
    • 合并重复的条件判断——不同的分支有相同的处理,提炼到分之外
  • 跟CI的持续集成
    • 1.自动化的回归并收集代码覆盖率
    • 2.查看代码覆盖率的历史记录
  • 精准的代码覆盖率
    • 1.识别出所有被修改的方法(新增,删除和修改)
    • 2.可以有效的查看那些被修改的方法是否被测试到
    • 3.通过圈复杂度衡量代码的质量
  • 突变测试