Jacoco 手動測試覆蓋率檢查

? 團隊中目前還沒有自動化測試的覆蓋,所以測試 team 想了解下手動測試的覆蓋率。于是才有了本片文章的產(chǎn)生。網(wǎng)上有很多文章是利用 Android 的 instrument 測試框架,然后通過命令來啟動app來進行測試。而且報告生產(chǎn)的時間點是在啟動的 activity 結(jié)束以后,在復雜場景下,是沒有辦法來捕捉到所有頁面的函數(shù)調(diào)用的。

本文中的方案是對一個新的 build type 來重載 Application 代碼,只在手動測試時候使用,對原來的代碼不會產(chǎn)生任何影響。 希望可以幫到你。本文的實例代碼可以到這里下載trevorwang/jacoco-manual-coverage

  1. 在你的工程目錄的buildscripts 下,新建一個 jacoco.gradle 的文件,添加如下代碼

    apply plugin: 'jacoco'  // 開啟 jacoco
    
    jacoco {    
        toolVersion = "0.8.3"  // 設置 jacoco 版本號
    }
    
    // 如果使用了 Robolectric 請務必添加如下代碼
    tasks.withType(Test) {
        jacoco.includeNoLocationClasses = true
    }
    
    
  2. 在 app 目錄下的 build.gradle 中添加代碼,來啟用腳本

    apply from: '../buildscripts/jacoco.gradle'
    
    
  3. 執(zhí)行 testDebugUnitTest 后,會在app/build/jococo/ 下看到 testDebugUnitTest.exec。 記住這個文件 我們會在后面用這個文件來生產(chǎn)報告。

    jacoco-executedata
  4. 創(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)報告生成的任務。

  5. 使用一下代碼替換上一個步驟中的TODO

     group = "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")
    
    
  6. 執(zhí)行一下 testDebugUnitTestCoverage 任務,我們就會在build 目錄下看到報告了

    image

    經(jīng)過以上步驟我們完成了一個jacoco 報告的生成過程。

關鍵步驟來了,如何在打包的app中開啟jacoco呢?

  1. 新建一個staging 的build type

        buildTypes {
            release {...}
    
            staging {
                initWith(debug)
                matchingFallbacks = ["debug"]
                testCoverageEnabled true    // 會將jacoco runtime打包至app中
            }
        }
    
    
  2. src 目錄下,與 main 通級,新建 staging 目錄

  3. 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"
        }
    }
    
    
  4. 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)]

  5. IDE Build Variants 下選擇 staging

    jacoco-build-variants
  6. 運行app并安裝到設備或者模擬器,操作一下,然后按返回鍵關閉所有的頁面,這時候會在 SD 卡目錄下的生成 jacoco/jacoco-coverage.ec 文件。

    jacoco-sd-location
  7. 復制 jacoco-coverage.ec 文件到項目根目錄下的 jacoco 文件夾

  8. 我們來修改jacoco的任務來生成最后的報告

                // 最開始我們生成的文件
                executionData = files([
                        "${project.buildDir}/jacoco/${testTaskName}.exec",
                        "${rootDir}/jacoco/jacoco-coverage.ec"   //增加一個數(shù)據(jù)源
                ])
    
    
  9. 運行 testStagingUnitTest 這樣就可以看到報告了

    jacoco-report-html

真香!~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容