基于gradle的jacoco offline模式

本文介绍了如何在Gradle项目中使用JaCoCo进行Java代码覆盖率测试,特别关注了如何在Android项目中实现离线模式,通过示例和配置详细步骤帮助读者解决在添加测试类到@PrepareForTest时覆盖率为0的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

jacoco的官网:EclEmma - JaCoCo Java Code Coverage Library

gradle语法:任务依赖 - Gradle 用户指南官方文档中文版 - UDN开源文档

在使用jacoco的时候,有时候需要将当前待测试的java类,添加到@PrepareForTest() 中,这会导致生成的行覆盖html中,当前类的覆盖率是为0,网上说比较好的解决办法是,使用jacoco offline模式,但是网上给的参考大部分都是maven文件,或者给的gradle文件使用不了。

github上提供了两个比较好的示例:

示例一:GitHub - MohammadAlavi1986/gradle-jacoo-offline-instrumentation

该示例是基于java程序的gradle配置,不使用android程序

示例二:https://github.com/h8MyJob/PowerMockJacocoDemo

该示例比较老了,运行起来有很多问题。

下面是我运行成功的配置(摸索了好久。。。)

其中,gradle版本是:gradle-7.5-bin.zip

顶层的build.gradle文件:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.4.1' apply false
    id 'com.android.library' version '7.4.1' apply false
}

module的build.gradle文件:

    //单元测试相关依赖
    testImplementation("org.powermock:powermock-module-junit4:2.0.7")
    testImplementation("org.powermock:powermock-module-junit4-rule:1.7.4")
    testImplementation("org.powermock:powermock-api-mockito2:2.0.7")
    testImplementation("org.powermock:powermock-core:2.0.7")
    testImplementation("org.mockito:mockito-core:3.11.2")
    testImplementation("junit:junit:4.13.2")
    testImplementation("android.arch.core:core-testing:1.1.1")
    testImplementation("org.mockito:mockito-inline:3.11.2")

最后 jacoco_offline.gradle 文件如下:

apply plugin: 'jacoco'

configurations {
    jacocoAnt
    jacocoRuntime
}

// 设置 jacoco offline 模式的Report目录
def jacocoOfflineDir = layout.buildDirectory.dir('jacocoOfflineReport')

jacoco {
    toolVersion = "0.8.11"  // 升级最新的版本
    reportsDirectory = jacocoOfflineDir
}

def offline_instrumented_outputDir = "$buildDir.path/intermediates/classes-instrumented/debugCoverage"

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
    jacoco.excludes = ['jdk.internal.*']
}

// 设置原java文件的目录
def coverageSourceDirs = [
        'src/main/java'
]

// 设置生成覆盖率报告时,需要排除的文件
def excludeFiles = [
        '**/R.class',
        '**/R$*.class',
        '**/Manifest*.*',
        '**/*Test*.*',
        'android/**/*.*',
        '**/BuildConfig.*',
        '**/**Bean.class',
        '**/inputmethod/*',
        '**/config/*.*',
        '**/flex/*.*',
        '**/AssetsUtils.class'
]

// 设置生成覆盖率报告时,仅仅支持的文件
def includeFiles = [
        '**/BuildConfig.*',
        '**/HCInfoUtil.*'
]

def classFileDir = fileTree(
        dir: 'build/intermediates/javac/debug/classes',
        excludes: excludeFiles
//        includes: includeFiles
)

// (1)生成jacoco动态覆盖率报告(jacoco属于动态检查)
task jacocoTestReport(type: JacocoReport, dependsOn: "test") {

    group = "Reporting"

    description = "Generate Jacoco coverage reports"

    classDirectories.from = classFileDir

    getSourceDirectories().setFrom(files(coverageSourceDirs))
    getExecutionData().setFrom(files('./build/jacoco/testDebugUnitTest.exec'))

}

jacocoTestReport {
    reports {
        xml.enabled  false
        html.enabled  true
        html.destination file("build/test-results/jacocoHtml")
    }
}

/**
 * jacoco offline instrumentation jacoco离线检查
 * 先生成原始的预检测的*.class,然后在此基础上生成.exec文件,而不是在运行中生成检测的class文件
 */
task createOfflineTestCoverageReport(dependsOn: ['clean', 'instrument', 'testDebugUnitTest']) {
    group = "Reporting"
    doLast {
        ant.taskdef(name: 'report',
                classname: 'org.jacoco.ant.ReportTask',
                classpath: configurations.jacocoAnt.asPath)
        ant.report() {
            executiondata {
                ant.file(file: "$buildDir.path/jacoco/testDebugUnitTest.exec")
            }
            structure(name: 'Example') {
                classfiles {
                    fileset(dir: "$project.buildDir/intermediates/javac/debug/classes")
                }
                sourcefiles {
                    fileset(dir: 'src/main/java')
                }
            }
            // offline模式生成的报告路径,此报告不能排除文件,一般不用,而是在之后的task中生成 html 报告
//            html(destdir: "$buildDir.path/reports/jacocoHtml")
        }
    }
}


gradle.taskGraph.whenReady { graph ->
    if (graph.hasTask(instrument)) {
        tasks.withType(Test) {
            doFirst {
                systemProperty 'jacoco-agent.destfile', buildDir.path + '/jacoco/testDebugUnitTest.exec'
                classpath = files(offline_instrumented_outputDir) + classpath + configurations.jacocoRuntime
            }
        }
    }
}

/*
 *  Instruments the classes per se
 */
task instrument(dependsOn:'compileDebugUnitTestSources') {
    doLast {
        println 'Instrumenting classes'

        ant.taskdef(name: 'instrument',
                classname: 'org.jacoco.ant.InstrumentTask',
                classpath: configurations.jacocoAnt.asPath)

        ant.instrument(destdir: offline_instrumented_outputDir) {
            fileset(dir: "$buildDir.path/intermediates/javac/debug/classes")
        }
    }
}

// 将 offline 模式生成的exec文件拷贝到另一个目录,与jacoco动态生成区分
task copyTask(dependsOn:['createOfflineTestCoverageReport']) {
    doLast{
        copy {
            from './build/jacoco/testDebugUnitTest.exec'
            into './build/jacoco/offline'
        }
    }
}

// 删除 offline 生成的html报告,防止执行当前生成报告未更新
task deleteReportTask(type: Delete) {
    doLast {
        delete jacocoOfflineDir.get().toString()+'/jacocoTestReportOffline'
    }
}

// 使用 offline 模式下的 exec 文件,生成html报告
task jacocoTestReportOffline(type: JacocoReport, dependsOn:['copyTask', 'deleteReportTask']) {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

    classDirectories.from = classFileDir

    getSourceDirectories().setFrom(files(coverageSourceDirs))
    getExecutionData().setFrom(files('./build/jacoco/offline/testDebugUnitTest.exec'))
}

// (2)生成 jacoco offline 模式的覆盖率报告
task GenerateJacocoOfflineCoverageReports(dependsOn:'jacocoTestReportOffline') {
    group = "Reporting"
    doLast {
        println '生成 jacoco offline 报告的路径:'
        println jacocoOfflineDir.get().toString() + '/jacocoTestReportOffline'
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值