团队中目前还没有自动化测试的覆盖,所以测试 team 想了解下手动测试的覆盖率。于是才有了本片文章的产生。网上有很多文章是利用 Android 的 instrument 测试框架,然后通过命令来启动app来进行测试。而且报告生产的时间点是在启动的 activity 结束以后,在复杂场景下,是没有办法来捕捉到所有页面的函数调用的。
本文中的方案是对一个新的 build type
来重载 Application 代码,只在手动测试时候使用,对原来的代码不会产生任何影响。希望可以帮到你。
1、在你的工程目录的 buildscripts 下,新建一个jacoco.gradle 的文件,添加如下代码
2、在 app 目录下的 build.gradle 中添加代码,来启用脚本
3、执行 testDebugUnitTest 后,会在app/build/jococo/ 下看到 testDebugUnitTest.exec。记住这个文件 我们会在后面用这个文件来生产报告。
4、创建jacoco 任务
Android gradle plugin 会生成不同的 variant, 所以我们要对不用的variant生成不用的任务来生产报告。
-
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 后面实现 }
-
}}
点击Sync Gradle后,gradle task 会增加两个任务 testDebugUnitTestCoverage
,testReleaseUnitTestCoverage,
接下来我们增加实现报告生成的任务。
5、使用一下代码替换上一个步骤中的TODO
-
group = "Reporting"
-
description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."
-
// 设置报告格式
-
reports {
-
html.enabled = true
-
xml.enabled = true
-
}
-
// 排除不需要统计的类
-
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、执行一下 testDebugUnitTestCoverage 任务,我们就会在build 目录下看到报告了
经过以上步骤我们完成了一个jacoco 报告的生成过程。
关键步骤来了,如何在打包的app中开启jacoco呢?
1、新建一个staging的build type
2、在src目录下,与 main 通级,新建 staging 目录
3、staging 目录下新建 java目录,并在 com.example.staging 包下新建 StagingApp.kt文件,代码如下:
-
package com.example.stagingimport android.Manifestimport android.app.Activityimport android.app.Applicationimport android.os.Bundleimport android.os.Environmentimport android.util.Logimport android.widget.Toastimport androidx.fragment.app.FragmentActivityimport com.tbruyelle.rxpermissions2.RxPermissionsimport java.io.Fileimport java.io.FileOutputStreamimport java.io.IOExceptionclass 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 目录访问权限
-
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被销毁后,生产报告文件
-
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 文件,内容如下
-
<?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>
5、IDE Build Variants 下选择 staging
6、运行app并安装到设备或者模拟器,操作一下,然后按返回键关闭所有的页面,这时候会在 SD 卡目录下的生成 jacoco/jacoco-coverage.ec 文件。
7、复制 jacoco-coverage.ec 文件到项目根目录下的 jacoco 文件夹
8、我们来修改jacoco的任务来生成最后的报告
9、运行 testStagingUnitTest 这样就可以看到报告了
感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取