Android使用JaCoCo生成代碼覆蓋率

本文目的

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搏嗡。

各工具比較.png

眾所周知,獲取覆蓋率數(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.png

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俩莽。

選擇你的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

image.png

執(zhí)行完畢后清蚀,會在以下路徑生成此 .ec結(jié)尾的文件,刪除即可
image.png

然后把剛才保存的coverage.ec復(fù)制到此路徑下
此路徑要和 jacoco.gradle中爹谭,路徑一致枷邪。

executionData.setFrom(files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"))
5.jacocoTestReport
image.png

雙擊執(zhí)行這個任務(wù),會生成我們最終所需要代碼覆蓋率報(bào)告诺凡,執(zhí)行完后东揣,我們可以在這個目錄下找到它

app/build/reports/jacoco/jacocoTestReport/html/index.html
image.png

總結(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市篓叶,隨后出現(xiàn)的幾起案子烈掠,更是在濱河造成了極大的恐慌,老刑警劉巖缸托,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件左敌,死亡現(xiàn)場離奇詭異,居然都是意外死亡俐镐,警方通過查閱死者的電腦和手機(jī)矫限,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叼风,你說我怎么就攤上這事取董。” “怎么了无宿?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵茵汰,是天一觀的道長。 經(jīng)常有香客問我孽鸡,道長蹂午,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任彬碱,我火速辦了婚禮豆胸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堡妒。我一直安慰自己配乱,他們只是感情好溉卓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布皮迟。 她就那樣靜靜地躺著,像睡著了一般桑寨。 火紅的嫁衣襯著肌膚如雪伏尼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天尉尾,我揣著相機(jī)與錄音爆阶,去河邊找鬼。 笑死沙咏,一個胖子當(dāng)著我的面吹牛辨图,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肢藐,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼故河,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吆豹?” 一聲冷哼從身側(cè)響起鱼的,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痘煤,沒想到半個月后凑阶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衷快,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年宙橱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡师郑,死狀恐怖哼勇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呕乎,我是刑警寧澤积担,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站猬仁,受9級特大地震影響帝璧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜湿刽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一的烁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诈闺,春花似錦渴庆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仁烹,卻和暖如春耸弄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卓缰。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工计呈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人征唬。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓捌显,卻偏偏與公主長得像,于是被迫代替她去往敵國和親总寒。 傳聞我的和親對象是個殘疾皇子扶歪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內(nèi)容