? 團隊中目前還沒有自動化測試的覆蓋,所以測試 team 想了解下手動測試的覆蓋率。于是才有了本片文章的產(chǎn)生。網(wǎng)上有很多文章是利用 Android 的 instrument 測試框架,然后通過命令來啟動app來進行測試。而且報告生產(chǎn)的時間點是在啟動的 activity 結(jié)束以后,在復雜場景下,是沒有辦法來捕捉到所有頁面的函數(shù)調(diào)用的。
本文中的方案是對一個新的 build type 來重載 Application 代碼,只在手動測試時候使用,對原來的代碼不會產(chǎn)生任何影響。 希望可以幫到你。本文的實例代碼可以到這里下載trevorwang/jacoco-manual-coverage
-
在你的工程目錄的
buildscripts下,新建一個jacoco.gradle的文件,添加如下代碼apply plugin: 'jacoco' // 開啟 jacoco jacoco { toolVersion = "0.8.3" // 設置 jacoco 版本號 } // 如果使用了 Robolectric 請務必添加如下代碼 tasks.withType(Test) { jacoco.includeNoLocationClasses = true } -
在 app 目錄下的
build.gradle中添加代碼,來啟用腳本apply from: '../buildscripts/jacoco.gradle' -
執(zhí)行
testDebugUnitTest后,會在app/build/jococo/下看到testDebugUnitTest.exec。 記住這個文件 我們會在后面用這個文件來生產(chǎn)報告。jacoco-executedata -
創(chuàng)建
jacoco任務Android gradle plugin 會生成不同的
variant, 所以我們要對不用的variant生成不用的任務來生產(chǎn)報告。project.afterEvaluate { android.applicationVariants.all { variant -> def variantName = variant.name def testTaskName = "test${variantName.capitalize()}UnitTest" tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: "$testTaskName") { // TODO 后面實現(xiàn) } } }點擊
Sync Gradle后,gradle task 會增加兩個任務testDebugUnitTestCoverage,testReleaseUnitTestCoverage,接下來我們增加實現(xiàn)報告生成的任務。 -
使用一下代碼替換上一個步驟中的
TODOgroup = "Reporting" description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build." // 設置報告格式 reports { html.enabled = true xml.enabled = true } // 排除不需要統(tǒng)計的類 def excludes = [ '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', 'androidx/**/*.*' ] // Java 類文件 def javaClasses = fileTree(dir: variant.javaCompiler.destinationDir, excludes: excludes) // Kotlin 文件 def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: excludes) classDirectories = files([javaClasses, kotlinClasses]) // 源文件 sourceDirectories = files([ "$project.projectDir/src/main/java", "$project.projectDir/src/${variantName}/java", "$project.projectDir/src/main/kotlin", "$project.projectDir/src/${variantName}/kotlin" ]) // 最開始我們生成的文件 executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec") -
執(zhí)行一下
testDebugUnitTestCoverage任務,我們就會在build 目錄下看到報告了image經(jīng)過以上步驟我們完成了一個jacoco 報告的生成過程。
關鍵步驟來了,如何在打包的app中開啟jacoco呢?
-
新建一個
staging的build typebuildTypes { release {...} staging { initWith(debug) matchingFallbacks = ["debug"] testCoverageEnabled true // 會將jacoco runtime打包至app中 } } 在
src目錄下,與main通級,新建staging目錄-
staging目錄下新建java目錄,并在com.example.staging包下新建StagingApp.kt文件,代碼如下:package com.example.staging import android.Manifest import android.app.Activity import android.app.Application import android.os.Bundle import android.os.Environment import android.util.Log import android.widget.Toast import androidx.fragment.app.FragmentActivity import com.tbruyelle.rxpermissions2.RxPermissions import java.io.File import java.io.FileOutputStream import java.io.IOException class StagingApp : Application() { override fun onCreate() { super.onCreate() Log.d(TAG, "StagingApp") registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { var activitySize = 0 override fun onActivityPaused(activity: Activity?) { } override fun onActivityResumed(activity: Activity?) { // 第一個activity 請求 SD card 目錄訪問權(quán)限 if (activitySize == 1) { (activity as? FragmentActivity)?.let { val rxPerm = RxPermissions(it) rxPerm.request(Manifest.permission.WRITE_EXTERNAL_STORAGE).subscribe({ result -> if (!result) { Toast.makeText( it, "You have to grant the permission to save coverage file", Toast.LENGTH_SHORT ).show() } }, { e -> e.printStackTrace() }) } } } override fun onActivityStarted(activity: Activity?) { } override fun onActivityDestroyed(activity: Activity?) { activitySize -= 1 if (activitySize <= 0) { //所有activity被銷毀后,生產(chǎn)報告文件 generateCoverageReport(createFile()) } } override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { } override fun onActivityStopped(activity: Activity?) { } override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { activitySize += 1 } }) } private fun generateCoverageReport(file: File) { Log.d(TAG, "generateCoverageReport():${file.absolutePath}") FileOutputStream(file, false).use { val agent = Class.forName("org.jacoco.agent.rt.RT") .getMethod("getAgent") .invoke(null) Log.d(TAG, agent.toString()) it.write( agent.javaClass.getMethod("getExecutionData", Boolean::class.javaPrimitiveType) .invoke(agent, false) as ByteArray ) } } fun createFile(): File { // SD card 下面 val file = File(Environment.getExternalStorageDirectory(), "jacoco/$DEFAULT_COVERAGE_FILE_PATH") if (!file.exists()) { try { file.parentFile?.mkdirs() file.createNewFile() } catch (e: IOException) { Log.d(TAG, "異常 : $e") e.printStackTrace() } } return file } companion object { const val DEFAULT_COVERAGE_FILE_PATH = "jacoco-coverage.ec" const val TAG = "StagingApp" } } -
staging目錄中新建一個AndroidManifest.xml文件,內(nèi)容如下<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.jacocomanual"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:name="com.example.staging.StagingApp"/> </manifest>最后效果如下:
[圖片上傳失敗...(image-c5b19a-1561108968313)]
-
IDE
Build Variants下選擇stagingjacoco-build-variants -
運行app并安裝到設備或者模擬器,操作一下,然后按返回鍵關閉所有的頁面,這時候會在 SD 卡目錄下的生成
jacoco/jacoco-coverage.ec文件。jacoco-sd-location 復制
jacoco-coverage.ec文件到項目根目錄下的jacoco文件夾-
我們來修改jacoco的任務來生成最后的報告
// 最開始我們生成的文件 executionData = files([ "${project.buildDir}/jacoco/${testTaskName}.exec", "${rootDir}/jacoco/jacoco-coverage.ec" //增加一個數(shù)據(jù)源 ]) -
運行
testStagingUnitTest這樣就可以看到報告了jacoco-report-html
真香!~~




