一.背景介紹
在產品客戶端的測試過程中,新功能測試以及迴歸測試在手工測試的情況下,即便是測試用例再詳盡,難免也可能會有疏漏之處。故使用代碼覆蓋率工具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) 測試工程添加廣播接收者
<receiver android:name="test.JacocoReceiver">
<intent-filter>
<action android:name="jacoco.test.action.START"/>
<action android:name="jacoco.test.action.CLEAR"/>
<action android:name="jacoco.test.action.END"/>
</intent-filter>
</receiver>
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