標籤:Android Jacoco 手工覆蓋率
作者:LightingContour
GitHub
剛好遇到GitHub當機……
不過還好,現在可以了
地址:https://github.com/LightingContour/LC-JacocoSample
引言
筆者這幾天搞了下Android覆蓋率,使用的Jacoco,打算做個手工覆蓋率的Demo。
一開始很開心啊,看到有各種教程,很簡單的樣子(誤)。但是一步步做下去,遇到了很多問題很多坑,一卡卡一天。
現在總算折騰出來了。這裡做一個最簡潔的手工覆蓋率Demo教程。寫給之後來這條路的大家,也是鞏固下自己學到的東西。
簡介
本篇主要介紹如何寫出通過Jacoco實現的覆蓋率Demo
Demo主Activity中有三個Button,前兩個分別都只是更改TextView內容。第三個點選會匯出覆蓋率。
另外為測試覆蓋率,還寫了個小小的彩蛋隱藏在前兩個Button程式碼中。
配套工具版本:Android Studio3.2
需要Get的知識
Jacoco簡介
JaCoCo是一個開源的覆蓋率工具(官網地址:http://www.eclemma.org/JaCoCo/),它針對的開發語言是java,其使用方法很靈活,可以嵌入到Ant、Maven中;可以作為Eclipse外掛,可以使用其JavaAgent技術監控Java程式等等。
很多第三方的工具提供了對JaCoCo的整合,如sonar、Jenkins等。
Instrumentation
Instrumentation和Acitivity很類似,但是沒有圖形介面。
可以把它理解為用於監控其他類的工具類。
繼承自以下教程
https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159
設計思路
1.先寫一個最基本的Activity配Xml
2.在這個Activity的基礎上新增儲存許可權,我們會將覆蓋率檔案儲存到SD卡上
3.新增覆蓋率程式碼
跟我一起動手做
建立基礎程式
1.建立一個Project,名為LC-JacocoSample。在引導頁面選擇Empty Activity。
2.Gradle Sync老是轉圈圈?請在Project的build.gradle中新增阿里雲國內maven地址。
然後,Sync一下,所有依賴就會很快Down下來啦。
另外需要注意,這裡請使用3.1.3版本的Gradle。後面會有坑~!
3.更改MainActivity位置。在Android檢視的com.lightingcontour.jacocotry下新增以下Package:app、test、Utils。
這是為了之後做準備。Utils用來存許可權獲取相關檔案,test用來存覆蓋率檔案。然後將MainActivity拖到app package下。
4.XML佈局檔案更新
三個Button,很簡單的配置
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".app.MainActivity">
<Button
android:id="@+id/Btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="24dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Test1" />
<TextView
android:id="@+id/Test1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.126"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.186" />
<TextView
android:id="@+id/Test2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="TEST2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.737"
app:layout_constraintStart_toEndOf="@+id/Test1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.176" />
<Button
android:id="@+id/Btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:text="Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Test2" />
<Button
android:id="@+id/Btn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
複製程式碼
5.MainActivity中新增程式碼,繫結Button,點選Button時會更改TextView的值等
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//定義layout中所用元件
public TextView A,B;
private int AClickedTime = 0;
private boolean easterEgg = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//賦值、繫結layout元件
A = (TextView) findViewById(R.id.Test1);
B = (TextView) findViewById(R.id.Test2);
findViewById(R.id.Btn1).setOnClickListener(this);
findViewById(R.id.Btn2).setOnClickListener(this);
findViewById(R.id.Btn3).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId())
{
case R.id.Btn1:
Toast.makeText(this,"點選了第一個按鈕",Toast.LENGTH_SHORT).show();
A.setText("點選了第一個按鈕");
//設定彩蛋:點選了第一個按鈕三次,flag為true
if (AClickedTime < 3)
{
AClickedTime++;
}else {
easterEgg = true;
}
break;
case R.id.Btn2:
Toast.makeText(this, "點選了第二個按鈕", Toast.LENGTH_SHORT).show();
B.setText("點選了第二個按鈕");
//設定彩蛋:flag為true時,執行以下操作
if (easterEgg == true)
{
A.setText("恭喜進入彩蛋");
B.setText("恭喜進入彩蛋");
}
break;
case R.id.Btn3:
Toast.makeText(this,"點選了第三個按鈕",Toast.LENGTH_SHORT).show();
break;
}
}
}
複製程式碼
6.測試一下,Build-Run。程式執行成功~第一部分完成!
新增SD卡儲存許可權
在進行覆蓋率程式碼編寫之前,我們還需要先搞定SD卡儲存許可權。
我們要先將覆蓋率檔案放到手機的SD卡中。然而大家知道,從Android6.0開始,不僅僅要在manifest中新增許可權,還要在程式中去動態申請獲取。那麼開始吧~
1.修改manifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
複製程式碼
2.新增PermissionUtils用於獲取儲存許可權
public class PermissionUtils {
// Storage Permissions 儲存許可權
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
/**
* Checks if the app has permission to write to device storage
* If the app does not has permission then the user will be prompted to
* grant permissions
*
* 檢查App是否有SD卡的寫入許可權
* 如果沒有,讓系統提醒授予
*
* @param activity
*/
public static void verifyStoragePermissions(Activity activity) {
// Check if we have write permission
try {
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
複製程式碼
3.在MainActivity啟動時新增許可權獲取
onCreate方法中呼叫
//動態申請SD卡讀取許可權
PermissionUtils.verifyStoragePermissions(this);
複製程式碼
呼叫Jacoco
1.1在test Package中新增FinishListener.java
package com.lightingcontour.lc_jacocosample.test;
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
複製程式碼
1.2在test Package中新增InstrumentationActivity.java
package com.lightingcontour.lc_jacocosample.test;
import android.util.Log;
import com.lightingcontour.lc_jacocosample.app.MainActivity;
public class InstrumentedActivity extends MainActivity {
public static String TAG = "InstrumentedActivity";
private FinishListener mListener;
public void setFinishListener(FinishListener listener) {
mListener = listener;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG + ".InstrumentedActivity", "onDestroy()");
super.finish();
if (mListener != null) {
mListener.onActivityFinished();
}
}
}
複製程式碼
1.3在test Package中新增JacocoInstrumentation.java
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;
//LOGD 除錯用布林
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() + "/coverage.ec";
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();
}
public void onStart() {
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 String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
}else {
return mCoverageFilePath;
}
}
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 (FileNotFoundException e) {
Log.d(TAG, e.toString(), e);
} catch (IOException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void UsegenerateCoverageReport() {
generateCoverageReport();
}
private boolean setCoverageFilePath(String filePath){
if (filePath != null && filePath.length() > 0) {
mCoverageFilePath = filePath;
}
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_IDENTIFIER,"\nError: " + msg);
}
@Override
public void onActivityFinished() {
if (LOGD) {
Log.d(TAG,"onActivityFinished()");
}
finish(Activity.RESULT_OK,mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath) {
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.更改App Model的Gradle檔案
2.1新增使用Jacoco
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.6.201602180812"
}
複製程式碼
2.2在Manifest中新增Jacoco許可權
<!-- Jacoco許可權-->
<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" />
複製程式碼
2.3新增task,用於將應用跑出來的覆蓋率ec檔案轉換為html可讀文件
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: '../app/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/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
複製程式碼
3.在MainActivity中增加呼叫生成Jacoco覆蓋率
3.1新增呼叫JacocoInstrumentation
import com.lightingcontour.lc_jacocosample.test.JacocoInstrumentation;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//新增下面這個呼叫
public JacocoInstrumentation jacocoInstrumentation = new JacocoInstrumentation();
複製程式碼
點選Button3的時候,就呼叫生成覆蓋率方法
case R.id.Btn3:
Toast.makeText(this,"點選了第三個按鈕",Toast.LENGTH_SHORT).show();
jacocoInstrumentation.UsegenerateCoverageReport();
break;
複製程式碼
準備完成!
那麼梳理一下
我們在test Package中新增了三個檔案用於Jacoco測試。
在Manifest中新增了許可權。
在Gradle中新增了用於將ec檔案生成html覆蓋率報告的task。
在MainActivity中新增了對JacocoInstrumentation的呼叫以及點選第三個Button時生成覆蓋率ec檔案。
接下來就是開跑了!~
輸出覆蓋率檔案
1./gradlew(windows用gradlew) installDebug 也可以用gradle檢視中的installDebug
記得連線真機或者AVD哈
命令列中輸入
adb shell am instrument -w -r com.lightingcontour.lc_jacocosample/.test.JacocoInstrumentation
複製程式碼
真機或者AVD會彈出做好的App,操作操作,點選下幾個按鈕之類的。
最後點選下Button3,就會匯出我們的覆蓋率檔案了。
點選完成後就可以退出App了,這樣後命令列中也會提示退出。
3.使用adb命令,複製到我們的電腦中。
我這兒用的是mac,直接複製到桌面上。
adb pull mnt/sdcard/coverage.ec ~/Desktop/123.ec
複製程式碼
Pull成功後,將得到的檔案放到task中指定的
$buildDir/outputs/code-coverage/connected/coverage.ec中,
也就是**.../LC-JacocoSample/app/build/outputs/code-coverage/connected**
然後使用Gradle檢視中的jacocoTestReport或者命令列,都行
最後,生成的報告在
.../LC-JacocoSample/app/build/reports/jacoco/jacocoTestReport/html 裡
恭喜大家,完成啦~
之後會原始碼上傳到Github,歡迎來點個Star!
有什麼問題可以儘量在github中提issue,我會在上面看。
遇到的坑記錄
- jacocoTestReport無輸出-app/build/intermediates/classes無內容
原因:gradle太新了,編譯檔案變更-Project:JacocoTry用gradle版本改成3.1.3 - Unable to read execution data file …/coverage.ec
解決方案:改toolVersion-jacoco {toolVersion = "0.7.6.201602180812"}
在其他帖子上也看到改到其他版本的……大家如果遇到了可以嘗試下
https://blog.csdn.net/roxxo/article/details/77720300#commentBox
參考資料
https://blog.csdn.net/qq_27459827/article/details/79514941?utm_source=blogxgwz0
https://blog.csdn.net/niubitianping/article/details/52918809
https://blog.csdn.net/itfootball/article/details/45644159
https://blog.csdn.net/o279642707/article/details/54576307
https://cloud.tencent.com/developer/article/1038055