本文目的
1掖棉、快速生成高覆蓋率的測試報(bào)告,免去研發(fā)手寫單元測試用例代碼的辛苦蒂窒,專注本職工作躁倒。
2荞怒、一套框架,不同項(xiàng)目可快速移植秧秉,前后不超過10分鐘
對此方案有極大幫助的帖子如下褐桌,少走了許多彎路:
https://javaforall.cn/162125.html
項(xiàng)目代碼地址:
鏈接: https://pan.baidu.com/s/19MpEIpTP_f1vl5fa0HcohA?pwd=edv6
提取碼: edv6
核心原理
官方生成代碼覆蓋率報(bào)告的流程
gradle為android提供的插件生成代碼覆蓋率的報(bào)告流程為首先在應(yīng)用目錄的生成coverage.ec文件(比如我們的應(yīng)用package為com.wuba.wuxian.android_0504,那么這個coverage.ec在測試完成時(shí)會在android系統(tǒng)的/data/data/com.wuba.wuxian.android_0504/目錄下生成),然后pull到本地的項(xiàng)目根目錄的build/outputs/code-coverage/connected 目錄下象迎,這個時(shí)候執(zhí)行createDebugCoverageReport 根據(jù)這個coverage.ec和build/intermediates/classes/debug 目錄下的class文件生成報(bào)告荧嵌,報(bào)告存放在項(xiàng)目根目錄下/build/outputs/reports/coverage/debug 下。
最重要的文件為coverage.ec砾淌,我們通過寫一套公共的代碼啦撮,生成此coverage.ec文件,手動從測試機(jī)中pull到配置文件需要解析的地方汪厨,然后利用AndroidStudio->gradle->reporting-> jacocoTestReport生成覆蓋率報(bào)告赃春。
主流測試方法
在android測試框架中,常用的有以下幾個框架和工具類:
JUnit4:Java最常用的單元測試框架
AndroidJUnitRunner:適用于 Android 且與 JUnit 4 兼容的測試運(yùn)行器
Mockito:Mock測試框架
Espresso:UI 測試框架劫乱;適合應(yīng)用中的功能性 UI 測試
UI Automator:UI 測試框架聘鳞;適合跨系統(tǒng)和已安裝應(yīng)用的跨應(yīng)用功能性 UI 測試
以上幾種方法費(fèi)時(shí)費(fèi)力,不僅需要手寫大量的測試代碼,還需要學(xué)習(xí)對應(yīng)框架的語法知識要拂,無疑增加了使用成本抠璃,所以全部PASS
工具選型
Android App 開發(fā)主流語言就是 Java 語言,而 Java 常用覆蓋率工具為 JaCoCo脱惰、Emma 和 Cobertura搏嗡。
眾所周知,獲取覆蓋率數(shù)據(jù)的前提條件是需要完成代碼的插樁工作拉一。而JaCoCo針對字節(jié)碼的插樁方式采盒,可分為兩種:
On-The-Fly(在線插樁):
1、JVM 中 通過 -javaagent 參數(shù)指定特定的 jar 文件啟動 Instrumentation 的代理程序蔚润;
2磅氨、代理程序在每裝載一個 class 文件前判斷是否已經(jīng)轉(zhuǎn)換修改了該文件,如果沒有則需要將探針插入 class 文件中嫡纠。
3烦租、代碼覆蓋率就可以在 JVM 執(zhí)行代碼的時(shí)候?qū)崟r(shí)獲取除盏;
優(yōu)點(diǎn):無需提前進(jìn)行字節(jié)碼插樁叉橱,無需考慮 classpath 的設(shè)置。測試覆蓋率分析可以在 JVM 執(zhí)行測試代碼的過程中完成者蠕。
Offliine(離線插樁):
1窃祝、在測試之前先對字節(jié)碼進(jìn)行插樁,生成插過樁的 class 文件或者 jar 包踱侣,執(zhí)行插過樁的 class 文件或者 jar 包之后粪小,會生成覆蓋率信息到文件大磺,最后統(tǒng)一對覆蓋率信息進(jìn)行處理,并生成報(bào)告探膊。
2杠愧、Offlline 模式適用于以下場景:
運(yùn)行環(huán)境不支持 Java agent,部署環(huán)境不允許設(shè)置 JVM 參數(shù)突想;
字節(jié)碼需要被轉(zhuǎn)換成其他虛擬機(jī)字節(jié)碼殴蹄,如 Android Dalvik VM 動態(tài)修改字節(jié)碼過程中和其他 agent 沖突究抓;
無法自定義用戶加載類
Android 項(xiàng)目使用的是 JaCoCo 的離線插樁方式
生成步驟
1猾担、
首先在自己的工程App,src/main/java 里面新增一個 JaCoCo 目錄 里面存放 3 個文件:FinishListener刺下、InstrumentedActivity绑嘹、JacocoInstrumentation。
FinishListener.java
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
InstrumentedActivity.java
public class InstrumentedActivity extends MainActivity {
public FinishListener finishListener;
public void setFinishListener(FinishListener finishListener) {
this.finishListener = finishListener;
}
@Override
public void onDestroy() {
if (this.finishListener != null) {
finishListener.onActivityFinished();
}
super.onDestroy();
}
}
JacocoInstrumentation.java
public class JacocoInstrumentation extends Instrumentation implements FinishListener {
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "";
private final Bundle mResults = new Bundle();
private Intent mIntent;
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
public JacocoInstrumentation() {
}
@Override
public void onCreate(Bundle arguments) {
Log.e(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.isFile() && file.exists()) {
if (file.delete()) {
Log.e(TAG, "file del successs");
} else {
Log.e(TAG, "file del fail !");
}
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.e(TAG, "異常 : " + e);
e.printStackTrace();
}
}
if (arguments != null) {
Log.e(TAG, "arguments不為空 : " + arguments);
mCoverageFilePath = arguments.getString("coverageFile");
Log.e(TAG, "mCoverageFilePath = " + mCoverageFilePath);
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
@Override
public void onStart() {
Log.e(TAG, "onStart def");
if (LOGD) {
Log.e(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 void generateCoverageReport() {
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.e(TAG, e.toString());
e.printStackTrace();
} 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;
}
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);
mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
+ msg);
}
@Override
public void onActivityFinished() {
if (LOGD) {
Log.e(TAG, "onActivityFinished()");
}
if (mCoverage) {
Log.e(TAG, "onActivityFinished mCoverage true");
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath) {
// TODO Auto-generated method stub
if (LOGD) {
Log.e(TAG, "Intermidate Dump Called with file name :" + filePath);
}
if (mCoverage) {
if (!setCoverageFilePath(filePath)) {
if (LOGD) {
Log.e(TAG, "Unable to set the given file path:" + filePath + " as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
2橘茉、
新建jacoco.gradle 文件工腋,這個文件提供給各個模塊使用jacoco.gradle
jacoco {
toolVersion = "0.8.2"
}
//源代碼路徑,你有多少個module畅卓,你就在這寫多少個路徑
def coverageSourceDirs = [
"$rootDir"+ '/app/src/main/java'
]
//class文件路徑擅腰,就是我上面提到的class路徑,看你的工程class生成路徑是什么翁潘,替換我的就行
def coverageClassDirs = [
"$rootDir"+ '/app/build/intermediates/javac/debug/classes'
]
//這個就是具體解析ec文件的任務(wù)趁冈,會根據(jù)我們指定的class路徑、源碼路徑拜马、ec路徑進(jìn)行解析輸出
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.setFrom(files(files(coverageClassDirs).files.collect {
fileTree(dir: it,
// 過濾不需要統(tǒng)計(jì)的class文件
excludes: ['**/R*.class',
])
}))
sourceDirectories.setFrom(files(coverageSourceDirs))
executionData.setFrom(files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"))
doFirst {
//遍歷class路徑下的所有文件渗勘,替換字符
coverageClassDirs.each {
path ->
new File(path).eachFileRecurse {
file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
}
項(xiàng)目的build.gradle 修改如下,注意不是App 下的build.gradle:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath'com.android.tools.build:gradle:4.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "org.jacoco:org.jacoco.core:0.8.2"
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.5'
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3、
app 中的build.gradle依賴這個 jacoco.gradle
apply from: 'jacoco.gradle'
配置 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="您的包名">
// 添加所需的權(quán)限
<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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
...
>
<activity
android:name=".jacoco.InstrumentedActivity"
android:label="InstrumentationActivity" />
</application>
<instrumentation
android:name=".jacoco.JacocoInstrumentation"
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:targetPackage="您的包名" />
</manifest>
生成測試報(bào)告
編譯好APK后
1.installDebug
選擇你的app -> Tasks -> install -> installDebug旺坠,安裝app到你的手機(jī)上。2.命令行啟動
adb shell am instrument com.android.test/com.android.test.jacoco.JacocoInstrumentation
3.點(diǎn)擊測試
這個時(shí)候你可以操作你的app扮超,對你想進(jìn)行代碼覆蓋率檢測的地方取刃,進(jìn)入到對應(yīng)的頁面,點(diǎn)擊對應(yīng)的按鈕出刷,觸發(fā)對應(yīng)的邏輯蝉衣,你現(xiàn)在所操作的都會被記錄下來,在生成的coverage.ec文件中都能體現(xiàn)出來巷蚪。當(dāng)你點(diǎn)擊完了病毡,根據(jù)我們之前設(shè)置的邏輯,當(dāng)我們MainActivity執(zhí)行onDestroy方法時(shí)才會通知JacocoInstrumentation生成coverage.ec文件屁柏,我們可以按返回鍵退出MainActivity返回桌面啦膜,生成coverage.ec文件可能需要一點(diǎn)時(shí)間哦(取決于你點(diǎn)擊測試頁面多少有送,測試越多,生成文件越大僧家,所需時(shí)間可能多一點(diǎn))
生成coverage.ec的邏輯可以自行修改雀摘,不必必須在onDestroy中回調(diào),宗旨就是你感覺APP功能你點(diǎn)的差不多了八拱,在某個地方調(diào)用生成coverage.ec阵赠。步驟2的目的是為了開啟手動測試的模式。
然后把生成的coverage.ec從調(diào)試的設(shè)備中取出來存在電腦的某個地方以備后用肌稻,代碼默認(rèn)存儲的地址為
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";
4.createDebugCoverageReport
執(zhí)行完畢后清蚀,會在以下路徑生成此 .ec結(jié)尾的文件,刪除即可
然后把剛才保存的coverage.ec復(fù)制到此路徑下
此路徑要和 jacoco.gradle中爹谭,路徑一致枷邪。
executionData.setFrom(files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"))
5.jacocoTestReport
雙擊執(zhí)行這個任務(wù),會生成我們最終所需要代碼覆蓋率報(bào)告诺凡,執(zhí)行完后东揣,我們可以在這個目錄下找到它
app/build/reports/jacoco/jacocoTestReport/html/index.html
總結(jié):
1、原理類似于偷梁換柱腹泌,AndroidStudio生成的解析文件我們不需要嘶卧,然后使用我們代碼生成的解析文件。
2凉袱、不同的項(xiàng)目芥吟,我們只需要修改特定的幾個地方就可完成報(bào)告的生成。
3绑蔫、jacoco.gradle
classDirectories.setFrom(files(files(coverageClassDirs).files.collect {
fileTree(dir: it,
// 過濾不需要統(tǒng)計(jì)的class文件
excludes: ['**/R*.class',
])
}))
過濾文件較多的話运沦,可以增加覆蓋率
4、App下的build.gradle
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
debuggable true //此開關(guān)要打開
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
testCoverageEnabled true
zipAlignEnabled false
minifyEnabled false
shrinkResources false //自動移除無用資源
}
}
參考鏈接:
1配深、https://blog.csdn.net/u011035026/article/details/125367266
2携添、https://blog.csdn.net/sanmi8276/article/details/116763511