Android Coverage
概念
简介
代码覆盖率是有系统化软件测试的一种方式。第一份出版的相关参考资料是 Miller 及 Malonely1963 年在 ACM 通讯上发表的论文
代码覆盖是飞行设备进行安全认证中的考量项目之一。其中航太产业标准要求这个值为 100%,因为其超高可靠度的产品需求,代码覆盖率的最终数值 100% 是诸多工业或者汽车产品的理想目标。
根据不同测试软件的测试程度,会用一种或者多种不同的覆盖率准则
### int foo (int x,
int y){ int z = 0;
if ((x>0) && (y>0)) { z = x; }
准则 | 特点 |
---|---|
函式覆盖率准则 | 函式覆盖率准则 |
函式覆盖率准则 | 具体函数中每一行代码(包括 z=x),执行一次,符合指令覆盖 100% |
判断覆盖率准则 | foo(1,1) 及 foo(0,1),满足判断覆盖率 100% |
条件覆盖率准则 | foo(1,1) foo(1,0) foo(0,0),所有条件都会出现成立及不成立的情形,满足条件覆盖 |
条件/判断准则 | true or false 至少都出现过一次 |
true or false 至少都出现过一次 | cell 6 |
多重条件覆盖准则 | 要求测试逻辑运算式中的所有组合 8 种 |
路径覆盖率准则 | 执行所有可能路径 |
循环覆盖率准则 | 循环执行过 0 次 一次 及一次以上 |
参数值覆盖率准则 | 一个方法的所有参数 都覆盖 |
一般而言代码覆盖率工具及函式库会影响代码性能,也会消耗内存和其他资源,无法在系统正常使用时进行测试。因此,一般只在开发阶段进行,不会将含覆盖率的代码交给客户。
``return z;}
综上:选择 jacoco
主流代码覆盖率工具都采用字节码插桩模式,通过钩子的方式来记录代码执行轨迹信息。其中字节码插桩又分为两种模式 On-The-Fly 和 Offine。On-The-Fly 模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。Offine 模式优点在于系统启动不需要额外开启代理,但是只能在系统停机的情况下才能获取代码覆盖率。
我们做的事情其实就是 “插桩”,真正的代码执行轨迹追踪和覆盖率的统计都是 jacoco 帮我们做到的,所以,我们只需要知道 该如何插桩即可。
1.编写生成覆盖率文件 coverage.ec 的类和方法
2.build.gradle(app)中新增 jacoco 插件
3.打开覆盖率统计开关(build.gradle 中实现)
4.覆盖率生成条件监听
5.安装 debug 版本
6.服务器生成 jacoco 报告
AndroidStudio+ 夜神模拟器 +jacoco
1.编写生成覆盖率文件 coverage.ec 的类和方法
package test;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class JacocoInstrumentation extends Instrumentation implements FinishListener {
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
private final Bundle mResults = new Bundle();
private Intent mIntent;
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
public JacocoInstrumentation() {
}
@Override
public void onCreate(Bundle arguments) {
Log.d(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath().toString() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (file.isFile() && file.exists()){
if (file.delete()){
System.out.println("file del successs");
}else {
System.out.println("file del fail !");
}
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, "异常 : " + e);
e.printStackTrace();
}
}
if (arguments != null) {
mCoverageFilePath = arguments.getString("coverageFile");
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
@Override
public void onStart() {
System.out.println("onStart def");
if (LOGD)
Log.d(TAG, "onStart()");
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}
private boolean getBooleanArgument(Bundle arguments, String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);
}
private void generateCoverageReport() {
OutputStream out = null;
try {
out = new FileOutputStream(getCoverageFilePath(), false);
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);
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
} else {
return mCoverageFilePath;
}
}
private boolean setCoverageFilePath(String filePath){
if(filePath != null && filePath.length() > 0) {
mCoverageFilePath = filePath;
return true;
}
return false;
}
private void reportEmmaError(Exception e) {
reportEmmaError("", e);
}
private void reportEmmaError(String hint, Exception e) {
String msg = "Failed to generate emma coverage. " + hint;
Log.e(TAG, msg, e);
mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
+ msg);
}
@Override
public void onActivityFinished() {
if (LOGD)
Log.d(TAG, "onActivityFinished()");
if (mCoverage) {
System.out.println("onActivityFinished mCoverage true");
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath){
// TODO Auto-generated method stub
if(LOGD){
Log.d(TAG,"Intermidate Dump Called with file name :"+ filePath);
}
if(mCoverage){
if(!setCoverageFilePath(filePath)){
if(LOGD){
Log.d(TAG,"Unable to set the given file path:"+filePath+" as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
2.修改 build.gradle 的配置文件(app)
apply plugin: 'jacoco'
jacoco{
}
def coverageSourceDirs = [
'../mobile/src/main/java'
]
task jacocoTestReport(type: JacocoReport,dependsOn:"connectedAndroidTest") {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
doFirst {
new File("$buildDir/intermediates/jacoco_instrumented_classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
修改 buildType
buildTypes {
debug {
/*打开覆盖率统计开关*/
testCoverageEnabled=true
debuggable=true
}
修改 application Manifest.xml
<instrumentation
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:name="test.JacocoInstrumentation"
android:targetPackage="My application"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
### 安装 debug 版本
在 app 中操作一番
### 生成报告
在 androidStudio 中的命令行中 执行 gradle jacocoTestReport ,就会在 build 目录下生成 report 啦。
至此,一个小的案例初步跑通,刚刚学习,有问题欢迎各位大神指正,后续会把他运用到公司的 app 进行尝试,还会继续更新的。