我们经常会遇到这样的问题。
1.手工测试覆盖率是多少?
2.UI自动化覆盖率是多少?
3.你怎么保证你覆盖了全部的场景?
其实这三个问题不难回答,可以从两个维度,
1.覆盖了需求的是多少,用例评审时,就是一个很好的统计。如果不到,会有补充,但是这个人为因素多,可能不全面。
2.看下功能测试或者UI自动化测试对于app 的代码的覆盖度是多少?
要想看到这个,我们必须要用工具呢,有了工具,我们才很好的去度量呢。我们选择Jacoco。那么如何来做呢。接下来,我们一起去解密,如何统计app 代码覆盖率。
首先,我们要在安卓代码中引入我们的依赖。在我们待测app的build.gradle做如下配置,引入我们的jacoco。
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.4" #依赖版本号
description("$buildDir/filescoverage.exec")#覆盖率文件的路径
reportsDir = file("$buildDir/reports/jacoco")#测试报告路径
}
配置完毕后,Android studio自动去给我们加载包。
接下来就是代码去实现了。我们去创建一个接口,FinishListener。接口主要有两个方法。
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
我们新起一个InstrumentedActivity,这个的目的呢,开始收入代码覆盖数据。
public class InstrumentedActivity extends LoginActivity {
public static String TAG = "InstrumentedActivity";
private FinishListener mListener;
public void setFinishListener(FinishListener listener) {
mListener = listener;
}
@Override
public void onDestroy() {
super.onDestroy();
super.finish();
if (mListener != null) {
mListener.onActivityFinished();
}
}
}
那么我们接下来去实现一个JacocoInstrumentation。
public class JacocoInstrumentation extends Instrumentation implements
FinishListener {
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "coverage.ec";
private final Bundle mResults = new Bundle();
private Intent mIntent;
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
@Override
public void onCreate(Bundle arguments) {
Log.d(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
File file = new File(getContext().getFilesDir(),"coverage.ec");
System.out.println(file.getAbsolutePath());
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() {
if (LOGD)
Log.d(TAG, "onStart()");
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}
private void generateCoverageReport() {
Log.d(TAG, "generateCoverageReport():" + getCoverageFilePath());
OutputStream out = null;
try {
out = new FileOutputStream(getContext().getFilesDir()+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);
} 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;
}
@Override
public void onActivityFinished() {
if (LOGD)
Log.d(TAG, "onActivityFinished()");
if (mCoverage) {
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);
}
}
}
这里,我们用到的就是统计覆盖的数据,最后生成文件,最后的文件生成是对应activity 销毁。
到这里,我们还需要去配置我们的响应的权限,因为要用到对应的权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
我们还需要吧对应的activity加载进来。
<activity android:label="InstrumentationActivity"
android:name="com.example.studayappp.test.InstrumentedActivity" />
由于是基于instrumentation,我们还需要对instrumentation进行相关的配置。
<instrumentation
android:handleProfiling="true"
android:name="com.example.studayappp.test.JacocoInstrumentation"
android:functionalTest="false"
android:label="tihis test"
android:targetPackage="com.example.studayappp">
</instrumentation>
配置完毕,我们就可以打包,然后用adb 执行下面的命令,去启动app
adb shell am instrument 包名/test.JacocoInstrumentation
启动app后,就可以正常测试。最后,我们返回或者杀掉应用。就可以产生对应的文件,路径在下面
/data/data/yourPackageName/files/coverage.ec
然后我们去pull下我们的覆盖率文件即可。最后呢,我们利用app的build.gradle配置一个任务即可
def coverageSourceDirs = [
'../app/src/main/java'
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/javac/debug/classes',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/filescoverage.exec")
doFirst {
new File("$buildDir/intermediates/javac/debug/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
如果配置中无法识别task任务中的方法的,可能是因为版本不一样,我的版本如下
这样我们去执行
gradlew.bat jacocoTestReport
就可以产生对应的测试报告了。
如果我们经过手工测试, 出来一个这样的报告,我们就可以告诉我的覆盖率是多少。那么反过来,我们也会发现,原来我们的用例也有覆盖不全的地方,即使我们经过用例的评审的阶段,还会出现覆盖不到的地方。但是我们满足了业务的100%覆盖,还有未覆盖的,我们需要斟酌覆盖的投入产出比。
代码覆盖率100% 不代表没有bug。代码没有覆盖100% 一定有bug
但是有可能你覆盖到80% 很轻松,往后增加5% 都费很大劲。那么我们可以去没有覆盖到的进行分析。不一定要做到代码100%全覆盖,尤其在功能测试阶段,代码100% 覆盖,会给大家增加很多的工作量,很有可能为了1%的覆盖率而耽误整体测试,得不偿失。覆盖率是为了提升我们测试用例的覆盖度,检验我们测试用例设计的全面性,它有两面性,合理引入覆盖率,合理选择一定的阈值。
本文介绍了Jacoco统计安卓app手工测试覆盖率的方法,这里没有做增量代码的覆盖率,没有做多人分工测试app,测试报告如何合并,如何启动不用Instrumentation直接启动app。后续的文章中,将会持续分享。