背景
新功能测试以及回归测试在手工测试的情况下,即便用例再为详尽,也会存在遗漏的用例。通过统计手工测试覆盖率的数据,可以及时的完善用例。本文以android jacoco及IOS Xcode 实现代码覆盖率方法。
Android 端实现
引入依赖
在app 目录下的build.gradle 引入jacoco 包
implementation 'org.jacoco:org.jacoco.core:0.8.7'//导入jacoco的版本包
相关类代码
FinishListener:
package 你的包名;
public interface FinishListener {void onActivityFinished();void dumpIntermediateCoverage(String filePath);
}
InstrumentedActivity
package 包名;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.util.Log;public class InstrumentedActivity extends MainActivity{public static String TAG = "IntrumentedActivity";private FinishListener mListener;public void setFinishListener(FinishListener listener) {mListener = listener;}@Overridepublic void onDestroy() {super.onDestroy();//Log.d(TAG + ".com.example.coveragetest.InstrumentedActivity", "onDestroy()");super.finish();if (mListener != null) {mListener.onActivityFinished();}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);}
}
JacocoInstrumentation
package 包名;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 implementsFinishListener {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;/*** Constructor*/public JacocoInstrumentation() {}@Overridepublic void onCreate(Bundle arguments) {Log.d(TAG, "onCreate(" + arguments + ")");super.onCreate(arguments);DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath().toString() + "/coverage.ec";Log.d(TAG, "DEFAULT_COVERAGE_FILE_PATH is : "+DEFAULT_COVERAGE_FILE_PATH);File file = new File(DEFAULT_COVERAGE_FILE_PATH);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();}@Overridepublic 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(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;}@Overridepublic void onActivityFinished() {if (LOGD)Log.d(TAG, "onActivityFinished()");if (mCoverage) {generateCoverageReport();}finish(Activity.RESULT_OK, mResults);}@Overridepublic void dumpIntermediateCoverage(String filePath){// TODO Auto-generated method stubif(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);}}}
修改 build.gradle 文件
- 增加jacoco插件,打开覆盖率开关
app目录下创建jacoco.gradle 如下:
apply plugin: 'jacoco'jacoco {toolVersion = "0.8.2"
}
def coverageSourceDirs = ['../app/src/main/java'
]def coverageClassDirs = ['../app/build/intermediates/javac/debug/classes'
]task jacocoTestReport(type: JacocoReport) {group = "Reporting"description = "Generate Jacoco coverage reports after running tests."reports {xml.enabled = truehtml.enabled = true}classDirectories.setFrom(files(files(coverageClassDirs).files.collect {fileTree(dir: it,// 过滤不需要统计的class文件excludes: ['**/R*.class','**/*$InjectAdapter.class','**/*$ModuleAdapter.class','**/*$ViewInjector*.class'])}))sourceDirectories.setFrom(files(coverageSourceDirs))executionData.setFrom(files("$buildDir/outputs/code-coverage/coverage.ec"))doFirst {new File("$buildDir/intermediates/javac/debug/classes/").eachFileRecurse { file ->if (file.name.contains('$$')) {file.renameTo(file.path.replace('$$', '$'))}}}
}
build.gradle 引入 jacoco 插件
apply from:'jacoco.gradle'
修改 AndroidManifest.xml
添加instrumentation 声明
<instrumentationandroid:handleProfiling="true"android:label="CoverageInstrumentation"android:name="com.example.coveragetest.JacocoInstrumentation"android:targetPackage="com.example.coveragetest"/>
测试步骤及报告生成
- 通过 adb shell am instrument 包名/包名.test.JacocoInstrumentation 启动 app;
- 进行 app 手工测试,测试完成后退出 App,覆盖率文件会保存在手机/data/data/yourPackageName/files/coverage.ec 目录
- 导出 coverage.ec 至 $buildDir/outputs/code-coverage 目录下;
- 使用 gradle jacocoTestReport 命令行分析覆盖率文件并生成 html 报告;
- 查看覆盖率报告
报告目录结构:
测试结果如下图:
PS:
- 绿色:表示行覆盖充分。
- 红色:表示未覆盖的行。
- 空白色:代表方法未修改,无需覆盖。
- 黄色棱形:表示分支覆盖不全。
- 绿色棱形:表示分支覆盖完全。
ios Xcode 实现
基本配置
- 配置targets如下图
- 配置test如下图
运行测试及查看覆盖率
- 运行测试,点击test
- 查看覆盖率