测试盲区终结者!一招搞定手动测试覆盖率,用Jacoco全流程监控

想象一下,你刚刚完成了一轮手动测试你的Java应用程序。你点击了所有功能,检查了bug,一切看起来都很好。但你怎么知道你的手动测试是否真的覆盖了你代码的所有关键部分?别再瞎猜了!Jacoco,这个强大的代码覆盖率工具,可以精确地告诉你,在你的测试(无论是手动的还是自动化的)过程中,哪些代码行被执行了。它就像一个“X光机”,帮你看清测试的盲区,让你的手动测试更有针对性、更高效。

Jacoco真的能测量手动测试的覆盖率吗?它是如何工作的?你需要做什么来设置它?为什么在手动测试中使用Jacoco这么重要?让我们深入细节,看看Jacoco如何帮助你确保你的手动测试努力没有白费。

团队中目前还没有自动化测试的覆盖,所以测试 team 想了解下手动测试的覆盖率。于是才有了本片文章的产生。网上有很多文章是利用 Android 的 instrument 测试框架,然后通过命令来启动app来进行测试。而且报告生产的时间点是在启动的 activity 结束以后,在复杂场景下,是没有办法来捕捉到所有页面的函数调用的。

本文中的方案是对一个新的 build type  来重载 Application 代码,只在手动测试时候使用,对原来的代码不会产生任何影响。希望可以帮到你。

简介

Jacoco是一个开源的Java代码覆盖率工具,通常用于自动化测试,但它也能帮助你检查手动测试的覆盖率。通过在Eclipse中启动应用并附加Jacoco代理,你可以记录手动测试过程中执行的代码行,然后生成详细的覆盖率报告。这对于确保你的手动测试全面覆盖代码至关重要,尤其是在自动化测试难以触及的场景中。

关键要点
  • Jacoco可以测量手动测试覆盖率,但需要特定设置,适合Eclipse用户。
  • 研究表明,Jacoco通过附加Java代理和分析覆盖数据,帮助发现手动测试的盲区。
  • 存在争议,部分开发者认为手动测试覆盖率难以量化,但Jacoco提供了一种可行方法。

观点与案例结合

Jacoco(Java Code Coverage)是一个开源的代码覆盖率工具,主要用于Java应用程序。它最初是为自动化测试(如单元测试和集成测试)设计的,但通过一些额外的配置,它也可以用于测量手动测试的覆盖率。以下是如何使用Jacoco来检查手动测试覆盖率的步骤:

  1. 启动应用带有Jacoco代理
    要开始,你需要在Eclipse中启动你的应用程序,并附带Jacoco Java代理。在Eclipse中,添加以下VM参数:-javaagent:C:\path\to\jacocoagent.jar=output=tcpserver。这告诉JVM使用Jacoco代理,并将覆盖数据输出到TCP服务器,可以从Eclipse连接。

    案例:假设你有一个简单的Java Web应用,你需要在Eclipse中运行它。添加Jacoco代理后,应用启动时会自动开始记录代码执行情况。

  2. 运行手动测试
    启动应用程序后,执行你的手动测试。这可能包括点击UI元素、输入数据或执行任何其他手动测试步骤。例如,你可以模拟用户注册、登录或购买商品等操作。

    案例:在你的Web应用中,你手动测试了用户注册功能,填写了表单并提交。你还测试了登录功能,尝试了正确的和错误的密码。

  3. 导入覆盖率会话
    完成测试后,在Eclipse中导入覆盖率会话。转到File -> Import -> Coverage Session,选择代理地址,输入会话名称,并选择要分析的代码。

    案例:你导入了一个名为“ManualTestSession”的会话,选择了你的Web应用的代码目录。

  4. 分析覆盖率报告
    一旦导入会话,你可以查看覆盖率报告。Jacoco会显示哪些代码行被执行,哪些没有。这可以帮助你识别你的手动测试中可能遗漏的区域。例如,你可能会发现某些方法或分支从未被执行过。

    案例:在你的注册功能中,你发现validatePassword()方法的某些分支(如密码长度不足)从未被测试到。这意味着你的手动测试需要更全面的场景覆盖。

注意事项

  • Jacoco插件仅适用于Eclipse IDE。
  • 执行数据必须与Eclipse中使用的类文件匹配;你不能在Eclipse之外构建和运行应用程序。
  • Eclipse会在关闭时终止JVM,因此使用output=tcpserver模式来获取结果。

案例分析

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、执行 testDebugUnitTest 后,会在app/build/jococo/ 下看到 testDebugUnitTest.exec。记住这个文件 我们会在后面用这个文件来生产报告。

图片

4、创建jacoco 任务

Android gradle plugin 会生成不同的 variant, 所以我们要对不用的variant生成不用的任务来生产报告。

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        defvariantName= variant.name        deftestTaskName="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

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.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() {
    overridefunonCreate() {
        super.onCreate()
        Log.d(TAG, "StagingApp")
        registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks {
            var activitySize =0
            overridefunonActivityPaused(activity: Activity?) {
            }

            overridefunonActivityResumed(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()
                        })
                    }
                }
            }

            overridefunonActivityStarted(activity: Activity?) {
            }

            overridefunonActivityDestroyed(activity: Activity?) {
                activitySize -=1

                if (activitySize <=0) {
                  //所有activity被销毁后,生产报告文件
                    generateCoverageReport(createFile())
                }
            }

            overridefunonActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
            }

            overridefunonActivityStopped(activity: Activity?) {
            }

            overridefunonActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
                activitySize +=1
            }

        })

    }

    privatefungenerateCoverageReport(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            )
        }
    }

    funcreateFile(): 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    }

    companionobject {
        constval DEFAULT_COVERAGE_FILE_PATH ="jacoco-coverage.ec"
        constval 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的任务来生成最后的报告:

// 最开始我们生成的文件
executionData = files([
    "${project.buildDir}/jacoco/${testTaskName}.exec",
    "${rootDir}/jacoco/jacoco-coverage.ec"  // 增加一个数据源
])

9、运行 testStagingUnitTest 这样就可以看到报告了

图片

社会现象分析

Jacoco 是 Java 系应用中最常用的代码覆盖率检测工具,它通过在代码运行时插桩,实现对每一行代码是否执行的跟踪。
在一次真实项目中,我们在功能测试后生成 Jacoco 报告,结果发现注册流程中两个异常逻辑分支完全未被触发。
如果不是报告“打脸”,我们可能还以为“都测完了”!

在现代软件开发中,测试覆盖率越来越被重视,尤其是在追求高质量和高可靠性的项目中。然而,手动测试往往被视为“模糊”的,因为它依赖于人的主观判断和经验。Jacoco的出现,让手动测试也能像自动化测试一样“量化”,这是一种趋势——从“经验主义”向“数据驱动”转变。越来越多的开发团队意识到,手动测试并不意味着“随意性”,而是需要与自动化测试一样严谨。Jacoco不仅帮助开发者发现代码中的“盲点”,还推动了测试方法论的全面升级,让手动测试与自动化测试相互补充,共同提升软件质量。

随着软件质量要求提升,“可衡量的测试”成为趋势,测试报告不能再是“主观描述”,而要基于数据说话。
但在大多数企业中,手动测试依旧占比不小,而这些测试成果往往缺乏量化依据。
Jacoco 的出现,正是帮助测试人员跨过“被动证明”的鸿沟,提供可信数据支撑。

结论

总之,Jacoco不仅限于自动化测试;它也可以用来测量手动测试的覆盖率。通过在应用程序中附加Jacoco代理并分析覆盖数据,你可以确保你的手动测试也对整体代码覆盖率做出贡献。结合自动化和手动测试覆盖率,可以创建更健壮的软件,捕捉可能被任何单一方法错过的bug。Jacoco不仅是工具,更是一种思维方式——让你的测试从“猜测”走向“精确”。

测试工作从来不是点几下按钮那么简单,而是一次又一次“为代码背书”的过程。
Jacoco,让每一次点操作都能留下可追溯的“足迹”。

“有了Jacoco,没有一行代码需要被遗漏——无论是通过自动化还是手动。今天就用精确的覆盖率洞察来赋能你的测试过程吧!”

测试的价值,不在你测了多少,而在你能证明测了多少。
用 Jacoco,把“瞎测”变“明测”,从此测试不再心虚!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值