一.背景介绍
在产品客户端的测试过程中,新功能测试以及回归测试在手工测试的情况下,即便是测试用例再详尽,难免也可能会有疏漏之处。故使用代码覆盖率工具Jacoco作为手工测试代码覆盖率的统计.
解决的问题: 通过查看测试代码覆盖率客观数据,来进一步完善业务测试场景,完善测试用例.
二.使用方法
1. 由于Android本身集成了Jacoco工具,故在其他工程如果需要生成代码测试覆盖率,则仅需要添加如下配置
buildTypes {
debug {
testCoverageEnabled true
... ...
}
}
2. 如果针对工程添加配置,在build apk时出现死循环锁( Waiting to acquire shared lock on daemon addresses registry),针对此种情况,需要增加如下配置:
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
}
app/build.gradle 增加配置(仅仅针对build不成功工程)
3. project/app/build.gradle 增加配置,此配置用于生成代码覆盖率报告
/*Java Code Coverage*/
apply plugin: 'jacoco'
def coverageSourceDirs = [
'../src/com/i/coverage'
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports"
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/classes/normal/debug',
excludes: ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*', '**/BuildConfig.*', '**/Manifest*.*']
)
executionData = files('./build/outputs/code-coverage/connected/flavors/NORMAL/coverage.ec')
}
(1) 生成测试报告需要添加插件jacoco
(2) 针对build不成功工程其jacoco版本必须和2步骤对应,否则会造成报告不能读
(3) coverageSourceDirs为测试代码路径,executionData为生成测试报告的数据源路径
4. 工程配置以及测试工具apk
(1) 测试工程添加广播接收者
public class JacocoReceiver extends BroadcastReceiver {
private static String TAG = "JacocoReceiver:";
private final static String ACTION_START = "jacoco.test.action.START";
private final static String ACTION_END = "jacoco.test.action.END";
private final static String ACTION_CLEAR = "jacoco.test.action.CLEAR";
private static String mCoverageFilePath;
private Context mContext;
@Override
public void onReceive(Context context, Intent intent) {
mContext = context;
if (intent != null) {
String aciton = intent.getAction();
if (ACTION_START.equals(aciton)) {
createFile();
} else if (ACTION_END.equals(aciton)) {
generateCoverageReport();
} else if (ACTION_CLEAR.equals(aciton)) {
deleteCoverageReport();
}
}
}
private void createFile() {
mCoverageFilePath = mContext.getFilesDir().getPath() + File.separator + "coverage.ec";
File file = new File(mCoverageFilePath);
if (!file.exists()) {
try {
file.createNewFile();
Log.i(TAG, "create report");
} catch (IOException e) {
Log.e(TAG, "IOException", e);
}
}
}
private void generateCoverageReport() {
Log.i(TAG, "generateCoverageReport():" + mCoverageFilePath);
OutputStream out = null;
try {
out = new FileOutputStream(mCoverageFilePath, 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.e(TAG, "Exception", e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
Log.e(TAG, "IOException", e);
}
}
Log.i(TAG, "jacoco end");
}
}
private void deleteCoverageReport() {
if (!TextUtils.isEmpty(mCoverageFilePath)) {
File file = new File(mCoverageFilePath);
if (file != null && file.exists() && file.isFile()) {
file.delete();
Log.i(TAG, "delete report");
}
}
}
}
(2) 测试工具发送广播,然后手动操作测试
public class MainActivity extends Activity implements View.OnClickListener {
private final static String ACTION_START = "jacoco.test.action.START";
private final static String ACTION_END = "jacoco.test.action.END";
private final static String ACTION_CLEAR = "jacoco.test.action.CLEAR";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
findViewById(R.id.start).setOnClickListener(this);
findViewById(R.id.end).setOnClickListener(this);
findViewById(R.id.clear).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start:
Intent intent = new Intent(ACTION_START);
sendBroadcast(intent);
Toast.makeText(this, "start listener coverage",1).show();
break;
case R.id.end:
Intent intentEnd = new Intent(ACTION_END);
sendBroadcast(intentEnd);
Toast.makeText(this, "create coverage report end",1).show();
break;
case R.id.clear:
Intent intentClear = new Intent(ACTION_CLEAR);
sendBroadcast(intentClear);
Toast.makeText(this, "clear coverage report success",1).show();
break;
default:
break;
}
}
}
(1) 测试工具用于生成测试代码数据源,在测试代码覆盖率时需要使用,生成数据源,路径/data/data/{packagename}/files/coverage.ec;
(2) 生成测试代码覆盖率报告时,需要拷贝该数据源到{projectname}/app/build/outputs/code-coverage/connected/flavors/NORMAL/coverage.ec
(3) 执行步骤3的task jacocoTestReport , 则在{projectname}/app/build/reports/jacoco/jacocoTestReport/html 生成测试报告,可针对性的查看.
三.报告样例
四.后续
以上是全量代码覆盖率的生成,适用于大改版的情况下去做全面的测试覆盖;
在日常的小版本迭代过程中,适合使用差量代码覆盖率的使用.
//TODO 差量代码覆盖率 https://testerhome.com/topics/5823