使用 Android Gradle 插件生成 XML 格式的 JaCoCo 单元测试覆盖率报告

Hal*_*ast 3 android code-coverage gradle kotlin

我正在尝试将测试覆盖率报告添加到 Android 应用程序项目中。该应用程序是用 Kotlin 编写的,并使用 Gradle(Groovy 变体)构建为 5 种风格。

使用以下 Gradle 插件:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}
Run Code Online (Sandbox Code Playgroud)

我一直在遵循许多不同的集成 JaCoCo 指南,但没有一个对我有用。当我意识到 Android gradle 插件有(严重记录不足)内置支持.exec通过添加enableUnitTestCoverage truebuildTypes { debug { ... } }.

但是,我需要 XML 格式的报告,以便使用此 Action将结果发布到 GitHub PR 。

我还发现,包含testCoverageEnabled在上面的同一块中添加了 Gradle 任务create${flavor}CoverageReport,它似乎可以完成我正在寻找的任务

createProdMainNetDebugCoverageReport - Creates test coverage reports for the prodMainNetDebug variant.
Run Code Online (Sandbox Code Playgroud)

但是,运行此任务会抱怨没有连接设备:

Execution failed for task ':app:connectedProdMainNetDebugAndroidTest'.
> com.android.builder.testing.api.DeviceException: No connected devices!
Run Code Online (Sandbox Code Playgroud)

如何使用 Android Gradle 插件获取 XML 格式的所有单元测试的代码覆盖率报告?或者通过更手动地配置它会更好吗?如何避免我的配置与现有配置发生冲突?

Hal*_*ast 7

@Fah 的另一个答案帮助找到了我最终使用的解决方案。此外,网络上有很多帖子描述人们如何以各种方式将 JaCoCo 集成到他们的项目中。

这些解决方案大多相似,但又略有不同,因为它们适合不同的项目。它们都没有直接适合我的设置。因此,虽然问题实际上并不是很复杂,但了解各个部分如何组合在一起以便针对您的特定项目进行正确的调整非常重要。以下是我作为 Gradle 新手尝试解释我在调查此问题时收集到的所有这些内容的尝试:

从根本上来说,该解决方案可以分解为

  1. 在单元测试运行器中启用覆盖率检测/收集。

  2. 定义一个或多个 Gradle 类型的任务JacocoReport(由插件定义jacoco?)。此任务会将测试运行器生成的执行数据收集到报告中,因此取决于实际运行测试的任务。

  3. 使用源、目标、执行文件等的正确位置配置任务。

  4. 添加任何其他 JaCoCo 自定义设置。

正如问题中提到的,Android Gradle 插件 (AGP) 已经包含 JaCoCo 支持。这意味着我们不需要显式启用该jacoco插件。看起来使用的版本可以使用配置

android {
    jacoco {
        version = "..."
    }
}
Run Code Online (Sandbox Code Playgroud)

这篇博文讨论了 AGP JaCoCo 支持的各种缺点,并提出了一个 Gradle 插件(Kotlin 中)来解决这些问题。

1.启用覆盖率检测

enableUnitTestCoverageAGP 通过设置适当的构建类型直接支持这一点:

android {
    ...
    defaultConfig {
        ...
        debug {
            ...
            enableUnitTestCoverage true
        }
    }
Run Code Online (Sandbox Code Playgroud)

还有一个(已弃用的和testCoverageEnabled组合)选项,它会额外创建我们在没有实际设备的 CI 中运行时无法使用的任务,如问题中所述。enableUnitTestCoverageenableAndroidTestCoverage

2.创建Gradle任务

Gradle 任务是使用该tasks.create方法定义的。任务具有名称、类型以及它所依赖的一组其他任务。在这种情况下,这意味着类似

tasks.create(name: "unitTestCoverageReport", type: JacocoReport, dependsOn: "testLocalDebugUnitTest") {
   // ... task implementation ...
}
Run Code Online (Sandbox Code Playgroud)

其中testLocalDebugUnitTest是运行启用了覆盖率收集的单元测试的现有任务。用于./gradlew tasks | grep -i test列出与测试相关的所有可用任务。

该任务可以在需要它的模块的文件中定义gradle.build(我做了什么),也可以在一个单独的文件中定义,以便从顶级模块应用,如 @fah 的答案,或者像某些帖子中那样显式导入下面列出。

由于它是用完整的编程语言编写的,因此有很多可能性可以以对当前项目有意义的任何方式围绕此构造设置抽象:

  • 此示例展示了如何将 JaCoCo 集成到类似 monorepo 的项目中,即具有多个 Gradle 模块的项目。它定义了一个通用的 JaCoCo Gradle 模块,可以将其导入到各个build.gradle文件中,为每个文件添加一个debugCoverage任务(假设它们有一个变体“调试”)。然后,它添加一个jacoco具有单个任务的虚拟模块allDebugCoverage,该任务收集所有其他模块生成的覆盖率数据debugCoverage(假设它们已首先运行)。JaCoCo 报告聚合插件看起来是一个可能的替代解决方案。

  • 此示例为每个变体创建一个任务。

  • 此示例为每种口味创建它。

定义任务后unitTestCoverageReport,可以使用 来运行它./gradlew unitTestCoverageReport。相关任务将首先自动(传递)执行。

3.配置Gradle任务

如上所述,任务实现设置了 JaCoCo 报告生成器的配置。正如@Fah 的回答中所见,相关字段是:

  • reports:用于配置生成哪种类型的报告的块。

  • sourceDirectories.from:原始源代码文件的位置。可选,但允许报告者将字节码指令映射回原始源,以便将它们包含到带有覆盖注释的报告中。

  • classDirectories.from:没有检测的编译类文件。执行数据引用这些类中的指令,因此它们对于定位实际指令是必需的。

  • executionData.from:“执行数据”的位置,即单元测试运行器在针对 JaCoCo 代理检测的类运行测试时收集的覆盖数据。

因此,虽然只需正确设置这些路径,但当构建过程记录不完善并且您对底层结构没有很好的理解时,这可能会具有挑战性。就我而言,它涉及猜测、在构建目录中搜索具有特定扩展名的文件以及阅读其他示例的组合。例如,我没有找到任何官方文档解释编译的 Kotlin 类进入${buildDir}/tmp/kotlin-classes.

fileTree尽管被命名为“目录”,但我见过的所有指南都使用带有一堆 s 的函数传递整个目录树exclude。对于我的 Kotlin Android 项目,以下配置就足够了:

tasks.create(name: "unitTestCoverageReport", type: JacocoReport, dependsOn: "testLocalDebugUnitTest") {
    group = "Verification" // existing group containing tasks for generating linting reports etc.
    description = "Generate Jacoco coverage reports for the 'local' debug build."

    reports {
        // human readable (written into './build/reports/jacoco/unitTestCoverageReport/html')
        html.enabled = true
        // CI-readable (written into './build/reports/jacoco/unitTestCoverageReport/unitTestCoverageReport.xml')
        xml.enabled = true
    }

    // Execution data generated when running the tests against classes instrumented by the JaCoCo agent.
    // This is enabled with 'enableUnitTestCoverage' in the 'debug' build type.
    executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/localDebugUnitTest/testLocalDebugUnitTest.exec"

    // Compiled Kotlin class files are written into build-variant-specific subdirectories of 'build/tmp/kotlin-classes'.
    classDirectories.from = "${project.buildDir}/tmp/kotlin-classes/localDebug"

    // To produce an accurate report, the bytecode is mapped back to the original source code.
    sourceDirectories.from = "${project.projectDir}/src/main/java"
}
Run Code Online (Sandbox Code Playgroud)

我认为 XML 报告对于进一步处理(例如将报告上传到工作服)最有用。

一些指南将任务定义包装在project.afterEvaluate. 我没有发现这样做的必要性,并且对 Gradle 的了解不够,无法理解它有什么不同。

4.额外的JaCoCo定制

要收集 example 执行的测试的覆盖率数据RobolectricTestRunner,还必须添加以下代码片段:

tasks.withType(Test) {
    jacoco.includeNoLocationClasses true
    jacoco.excludes = ['jdk.internal.*']
}
Run Code Online (Sandbox Code Playgroud)

这篇文章建议使用testOptions块来设置这样的配置。

最后,该jacoco插件还包括一个配置,jacocoTestCoverageVerification如果不满足某些覆盖指标,则构建失败。

GitHub 操作

以下示例工作流程运行 Gradle 任务并使用以下命令将生成的 HTML 报告作为工作流程工件上传madrapps/jacoco-report

android {
    jacoco {
        version = "..."
    }
}
Run Code Online (Sandbox Code Playgroud)