踩坑日记之Gradle自定义JacocoReport跟Test task

Android 使用 jacoco 统计代码行覆盖率 无需安装运行 Android 使用 jacoco 统计代码行覆盖率 无需安装运行前言正文1、项目 build.gradle2、jacoco-report.gradle3、 app/*module的build.gradle4、 测试用例5、 运行 task jacocoTestReport5、 查看报告6、 小花招:快速提升代码覆盖率 前言 jacoco是Java Code Coverage的缩写,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理介绍,在网上有很多文章,感兴趣的同学可以去找别的博客看看,这里不 阅读详情

起因

最近新写了一个项目,为了更好的保证项目输出的质量,引入了单元测试覆盖率统计框架Jacoco。由于gradle官网上的案例只有几个默认的task(test、JacocoTestReport等)的设置,而我希望能够额外为不同的层提供单独的test,也就发生了接下来这些有趣的事儿。

1. 为什么JacocoTestReport总是被SKIPPED

首先,我写了一个自定义的test。并且通过将finalizedBy指定为JacocoTestReport来让test执行完毕后自动执行JacocoTestReport。自定义test以及JacocoTestReport 如下:

//自定义test
task serviceTest(type: Test) {
    useTestNG()
    useJUnitPlatform()

	finalizedBy jacocoTestReport

    jacoco {
        enabled = true
        //指定原始数据文件位置
        destinationFile = layout.buildDirectory.file("jacoco/${taskName}.exec").get().asFile
        includes = ['xxxservice']
        excludeClassLoaders = []
        includeNoLocationClasses = false
        sessionId = "<auto-generated value>"
        output = JacocoTaskExtension.Output.FILE
    }
}

jacocoTestReport {
    // tests are required to run before generating the report
    dependsOn serviceTest
		
	//指定覆盖率统计数据文件。
	executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: ['xxxservice'])
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoReport')
    }
}

看起来似乎没啥问题,该做的都做了。但是执行的时候程序却跳过了jacocoTestReport。当时特别纳闷,就去网上提了个问题。最后gradle官方的工作人员给了答复,大概意思是executionData对应的文件不存在,在jacocoTestReport里加上onlyIf = {true} 可以保证jacocoTestReport会执行,并且执行最终会报出test.exec文件不存在的错误。ok,有方向了,那就一个一个来分析。

2. task里的onlyIf

事实上,onlyIf决定了task是否被执行。同时,onlyIf可以设置多个规则判断,如果所有的规则判断都返回为true,才会执行task。否则,跳过task

2.1 SkipOnlyIfTaskExecuter

让我们进入SkipOnlyIfTaskExecuter一探究竟:

public class SkipOnlyIfTaskExecuter implements TaskExecuter {
	@Override
    public TaskExecuterResult execute(TaskInternal task, TaskStateInternal state, TaskExecutionContext context) {
        boolean skip = !task.getOnlyIf().isSatisfiedBy(task);
				
		//跳过task
        if (skip) {
            return TaskExecuterResult.WITHOUT_OUTPUTS;
        }
				
		//执行task
        return executer.execute(task, state, context);
    }

	//遍历所有规则
	public boolean isSatisfiedBy(T object) {
        Spec<? super T>[] specs = getSpecsArray();
        for (Spec<? super T> spec : specs) {
            if (!spec.isSatisfiedBy(object)) {
								//只要有一个规则不通过,则返回false
                return false;
            }
        }
        return true;
    }
}

2.2 onlyIf用法

那么,我么怎么使用onlyIf呢?

2.2.1 自定义规则作为唯一判断条件

这里用=即可。

task foo{
	onlyIf = {true}
}
//或者
foo.onlyIf = {true}

这里我们可以从gradle源码AbstractTask#setOnlyIf 看出来

	public void setOnlyIf(final Closure onlyIfClosure) {
        taskMutator.mutate("Task.setOnlyIf(Closure)", new Runnable() {
            @Override
            public void run() {
				//设置onlyIfClosure并覆盖原有的onlyIfSpec
                onlyIfSpec = createNewOnlyIfSpec().and(onlyIfClosure);
            }
        });
    }

	//创建一个新的默认返回为true的element
	private AndSpec<Task> createNewOnlyIfSpec() {
        return new AndSpec<Task>(new Spec<Task>() {
            @Override
            public boolean isSatisfiedBy(Task element) {
				//调用方: task.isSatisfiedBy(task)
                return element == AbstractTask.this && enabled;
            }
        });
    }

2.2.2 自定义规则作为条件判断的一部分

去掉=即可

task foo{
	onlyIf {true}
}
//或者
foo.onlyIf {true}

源码部分:

	public void onlyIf(final Closure onlyIfClosure) {
        taskMutator.mutate("Task.onlyIf(Closure)", new Runnable() {
            @Override
            public void run() {
				//插入
                onlyIfSpec = onlyIfSpec.and(onlyIfClosure);
            }
        });
    }

这种方式可以方便我们定义多个onlyIf块,同时也可以保证不会覆盖系统默认的onlyIf。接下来我们来聊聊jacocoTestReport预置的onlyIf

3. jacocoTestReportonlyIf为什么"默认"是false

从上面的分析可知jacocoTestReport会被跳过显然是isSatisfiedBy返回了false。而正常情况通过createNewOnlyIfSpec()初始化的specisSatisfiedBy返回的必然是true。所以,肯定是什么地方添加了额外的spec。我们先看看JacocoReport这种类型的Task是如何初始化的:

/**
 *  JacocoReport extends JacocoReportBase
 *  JacocoReportBase extends AbstractTask
 */
public abstract class JacocoReportBase extends JacocoBase {

	public JacocoReportBase() {
		//添加spec
        onlyIf(new Spec<Task>() {
            @Override
            public boolean isSatisfiedBy(Task element) {
                return Iterables.any(getExecutionData(), new Predicate<File>() {
                    @Override
                    public boolean apply(File file) {
                    	//返回文件是否存在
                        return file.exists();
                    }
                });
            }
        });
    }
}

可以看到,JacocoReport在初始化时会添加一个验证规则,如果getExecutionData()对应的文件都存在,则返回true,否则返回false 。那么,onlyIf"默认"为false,显然是getExecutionData()返回的文件路径中有的文件不存在。接下来我们研究为什么会有文件不存在的问题。

4. jacocoTestReportexecutionData为什么会默认包含"test.exec"

经过上述分析,我们知道只要为task设置onlyIf={true}之后,task就必然会执行。通过添加onlyIf={true},jacocoTestReport开始运行。这是突然又得到一个错误:

Unable to read execution data file /xxx/test.exec

很奇怪,明明我们设置的executionDataserviceTest.exec,为什么jacocoTestReport会去查找test.exec呢?显然,这又是被预置的行为。所以,我们先来看看jacocoTestReport这个Task是在哪里创建出来的:

public class JavaPlugin implements Plugin<Project> {
		public static final String TEST_TASK_NAME = "test";
}

public class JacocoPlugin implements Plugin<Project> {

	private void addDefaultReportAndCoverageVerificationTasks(final JacocoPluginExtension extension) {
        project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {

			//获取test Task
            TaskProvider<Task> testTaskProvider = project.getTasks().named(JavaPlugin.TEST_TASK_NAME);
			//初始化jacocoTestReport Task
            addDefaultReportTask(extension, testTaskProvider);
        });
    }
		
	private void addDefaultReportTask(final JacocoPluginExtension extension, final TaskProvider<Task> testTaskProvider) {
		//testTaskName是常量test
        final String testTaskName = testTaskProvider.getName();
		//注册jacocoTestReport Task
        project.getTasks().register(
            "jacoco" + StringUtils.capitalize(testTaskName) + "Report",
            JacocoReport.class,
            reportTask -> {
				//定义该Task的默认配置项
				...
				//设置executionData为test里定义的destinationFile
                reportTask.executionData(testTaskProvider.get());
                ...
            });
    }
}

public abstract class JacocoReportBase extends JacocoBase {

	public void executionData(Task... tasks) {
        for (Task task : tasks) {
			//拿到传入task里定义的jacoco extension,也就是jacoco{}定义的内容
            final JacocoTaskExtension extension = task.getExtensions().findByType(JacocoTaskExtension.class);
            if (extension != null) {
                executionData(new Callable<File>() {
                    @Override
                    public File call() {
						//设置executionData为task里定义的destinationFile
                        return extension.getDestinationFile();
                    }
                });
                mustRunAfter(task);
            }
        }
    }
}

可以看到,JacocoPlugin在初始化时会注册一个类型为JacocoReport而名称为jacocoTestReportTask,这个TaskexecutionData默认设置为test Task的jacoco extension中定义的destinationFile。而上文中的testjacoco extension里定义的destinationFile正好是layout.buildDirectory.file("jacoco/${taskName}.exec").get().asFile。所以到这里其实也就真相大白了。

事实上,类型为TestTaskjacoco extensiondestinationFile默认值就是layout.getBuildDirectory().file("jacoco/" + taskName + ".exec").map(RegularFile::getAsFile)。所以,如果我们没有定义这个值,也会报出同样的错误。源码部分如下:

public class JacocoPlugin implements Plugin<Project> {
	public void apply(Project project) {
        JacocoPluginExtension extension = project.getExtensions().create(PLUGIN_EXTENSION_NAME, JacocoPluginExtension.class, project, agent);
        //为test设置默认jacoco extension
        applyToDefaultTasks(extension);
        //初始化默认jacocoTestReport
        addDefaultReportAndCoverageVerificationTasks(extension);
    }

	private void applyToDefaultTasks(final JacocoPluginExtension extension) {
		//拿到所有test Task并配置jacoco extension
        project.getTasks().withType(Test.class).configureEach(extension::applyTo);
    }
}

public class JacocoPluginExtension {
	public <T extends Task & JavaForkOptions> void applyTo(final T task) {
        final String taskName = task.getName();
        final JacocoTaskExtension extension = task.getExtensions().create(TASK_EXTENSION_NAME, JacocoTaskExtension.class, objects, agent, task);
		//设置文件名为${taskName}.exec。test Task对应的就是test.exec
        extension.setDestinationFile(layout.getBuildDirectory().file("jacoco/" + taskName + ".exec").map(RegularFile::getAsFile));
    }
}

5. 怎么实现最初的目标

既然我们知道了jacocoTestReport里预设了executionData,那么要实现最初目标,我们只需要在执行task时把它清空并重新设置,或者自定义一个干净的JacocoReport即可。

5.1 清空并重新设置

jacocoTestReport {
    // tests are required to run before generating the report
    dependsOn serviceTest
		//清空预设的executionData
		((DefaultConfigurableFileCollection) executionData).filesWrapper.clear();
		//指定覆盖率统计数据文件。
		executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: ['xxxservice'])
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoReport')
    }
}

5.2 自定义一个干净的JacocoReport

task jacocoServiceTestReport(type: JacocoReport) {
    // tests are required to run before generating the report
    dependsOn serviceTest

    //自定义的不会初始化源码目录,需要手动指定
    sourceSets sourceSets.main

    executionData(layout.buildDirectory.file("jacoco/${serviceTest.name}.exec").get().asFile)

    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, include: project.ext.serviceSources)
        }))
    }
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir(project.ext.reportDir as String)
    }

    onlyIf = { true }
}

一些唠叨(没什么营养,可跳过)

当时碰到这个问题,没有头绪。就跑到StackOverFlow上瞎逛过一圈。其中有一个相同的问题,但下面的解答不对。最后还是我解决问题后跑到下面给了解答(作为一个gradle菜鸟,能帮助到他人,很开心😃!)。

总结

因为对gradlejacoco不怎么熟,当时看到唯一的提示信息:jacocoTestReport SKIPPED我是一脸懵逼的。好在当时从stackOverFlow以及官方那里得到了一个切入点:onlyIf={true}。让问题最终得以解决。

另一方面,我觉得官方在这一块的sample略显简陋,这些默认行为至少应该提一下,否则很容易让人摸不着头脑。所以我翻文档终究没有翻出解决方案(如果有哪位朋友发现官方文档有过这方面的描述,欢迎指出👏)。

github

Java单元覆盖率工具JaCoCo详细理解和使用(配置+示例) 一、代码覆盖率理解 代码覆盖(Code coverage)是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。 简单来理解,就是单元测试中代码执行量与代码总量之间的比率。 Java常用的单元测试覆盖率框架有:JaCoCo、EMMA和Cobertura,本篇文章主要介绍JaCoCo的使用。 二、JaCoCo理解 JaCoCo官方文档:https://www.eclemma.org/jacoco/trunk... 阅读详情

相关推荐

jacoco和sonar

jacoco sonar

javajy的博客 1258

java中Gradle项目使用JacocoTestReport

apply plugin: 'java' apply plugin: 'jacoco' //编译时自动执行jacocoTestReport,生成报告;报告路径build/reports/jacoco/test/html/index.html test { finalizedBy jacocoTestReport // report is always generated after tests run } jacocoTestReport { dependsOn test // tests

qq_43137849的博客 2989

TOPSIS与熵权法实战:从数学建模到Python实现(附国赛案例分析)

本文详细解析了TOPSIS(逼近理想解排序法)与熵权法在数学建模中的协同应用,并通过Python代码实现完整流程。文章以国赛案例为背景,从指标正向化、数据标准化、熵权法计算客观权重,到TOPSIS综合排序,手把手指导如何解决多指标决策问题,为数据分析与建模提供了一套可复用的实战工具箱。

weixin_29079643的博客 206

解决 执行jacocoTestReport生成报告时 报Unable to read execution data file coverage.ec

1. 问题 1.1 运行  jacocoTestReport gradle jacocoTestReport$ gradle jacocoTestReport报如下错* What went wrong:Execution failed for task ':app:jacocoTestReport'.> Unable to read execution data file E:\app\b

roxxo的博客 8471

jacoco生成测试报告

一、Gradle配置信息 apply plugin: "jacoco" jacoco { toolVersion = "0.8.4" reportsDir = file("$buildDir/customJacocoReportDir") } jacocoTestReport { reports { html.enabled true csv.enabled false xml.enabled true xml.de

heijunwei的博客 2890

Java单元测试实践-23.Gradle单元测试日志、报告与JaCoCo代码覆盖率

Java单元测试实践-00.目录(9万多字文档+700多测试示例) https://blog.csdn.net/a82514921/article/details/107969340 1. Gradle测试日志、报告与覆盖率 1.1. 测试日志 参考 https://docs.gradle.org/current/userguide/java_testing.html#sec:test_execution ,或 https://docs.gradle.org/current/dsl/org.gradle.a

adrninistrat0r的博客 4258

jacoco多版本报告合并

jacoco提供了一个merge命令可以给我方便的合并代码无变更时的报告,但是一旦代码发生变化,则无法通过jacoco进行直接合并,原因在《》中已经说明,那么针对一次功能测试,势必会进行多轮,每一轮针对变更的数据进行覆盖率增量报告,这是没有什么问题的,但是对于一些需求可能需要展示整个测试的覆盖报告,怎么将变更类的报告也进行合并呢?这就是本文讨论的问题。

tushuping的博客 7267

使用jacoco插件时提示skipping jacoco execution due to missing data file

skipping jacoco execution due to missing data file

tong1132的博客 6932

jacoco生成覆盖率报告的几种方式

1.本地跑maven项目集成jacoco获取单元测试代码覆盖率,在pom.xml的build标签里面增加该插件              &lt;plugin&gt;                 &lt;groupId&gt;org.jacoco&lt;/groupId&gt;                 &lt;artifactId&gt;jacoco-maven-plugin&lt...

kang123488的博客 1万+

【亲测免费】 JaCoCo报告生成器使用教程

JaCoCo报告生成器使用教程 1. 项目介绍 jacoco-report 是一个GitHub Action,用于在Pull Request中发布JaCoCo报告作为评论。该Action允许用户自定义通过百分比,包括修改模块的文件和整个项目。通过这个Action,用户可以查看Pull Request中仅更改文件的覆盖率。 主要功能 在Pull Request中发布JaCoCo报告。 支持自定义通...

gitblog_00214的博客 868

配置 jacoco 以提供更合理的测试覆盖率

最近在做一些代码的重构和基础库的迁移,这样的工作绝大部分时候不产生新的功能点,每次更换了类库后也都会将原来对应的测试同步迁移过来,保证新的代码和原来的代码一样工作。不过在迁移的过程中我发现 jacoco 所提示的代码覆盖率越来越低,让我很慌。为了搞明白这是啥原因,做了一些调研,这里把一些结论记录在这里加深印象,也便于后续查看。 代码测试覆盖率是什么意思 在 Intro to JaCoCo 这里讲的非常明白了,代码测试覆盖率(或者说代码覆盖率)讲的是在跑测试的时候,到底有多少代码被执行了。按照粒度来分可以有

eisen 的博客 2334

使用 JaCoCo 生成测试覆盖率报告

使用 JaCoCo 生成覆盖率时,可以使用 on-the-fly 方式,对程序的原代码是无侵入式的,只是在启动 jar 包时,增加一个参数就行,可确保测试人员测试的代码,和上线到生产环境的代码一致。

QQ156881887的专栏 5094

jacoco测试代码覆盖率_使用Jacoco测量多模块Android项目中的单元测试覆盖率:第2部分

jacoco测试代码覆盖率In the first article, we discovered one of the two key Gradle commands which come with Jacoco plugin — jacocoTestReport. As you now know, it can be used to measure code coverage in your p...

weixin_26739079的博客 1015

使用 Jacoco 实现 Android 端手工测试覆盖率统计

由于现在单元测试在我们这小公司无法推行,且为了解决新功能测试以及回归测试在手工测试的情况下,即便用例再为详尽,也会存在遗漏的用例。通过统计手工测试覆盖率的数据,可以及时的完善用例。 经过了解准备使用Jacoco完成这个需求.Jacoco是Java Code Coverage的缩写,在统计完成Android代码覆盖率的时候使用的是Jacoco的离线插桩方式,在测试前先对文件进行插桩,在手工测试过程中...

hualusiyu的专栏 2251

jacoco插件配置生成单元测试覆盖率报告

maven的phase和goal 在讲jacoco配置之前,先讲一下maven插件配置的phase和goal。 phase(插件阶段)由goal(插件目标)构成。phase其实就是goal的容器,实际被执行的都是goal。phase被执行时,实际执行的都是被绑定到该phase的goal。比如执行 mvn package(这里的package为phase), 就包含了validate、 compile、test、package四个goal(目标)。 一个goal(目标)代表一个具体的task。goal可以属于

weixin_38384296的博客 5163

使用webdriver实现基于GUI的测试【建议收藏】

前面介绍了如何使用Selenium2Library实现基于GUI的测试,Selenium2Library是Selenium的插件,而本文介绍的webdriver是playwright的插件

569

jacoco:java代码覆盖率实践

java代码覆盖率实践

一个Tester的博客 7755

超详细!手把手教你用 JaCoCo 生成单测覆盖率报告!

我们都知道 Spock 是一个单测框架,其特点是语法简明。但当我们使用 Spock 写了一堆单元测试之后,如何生成对应的单测覆盖率报告呢?一般来说,我们会使用两个插件来一起完成单测覆盖率报告的生成,分别是:Maven Surefire PluginJaCoCo Plugin其中 Maven Surefire Plugin 是用来在 Maven 的编译阶段运行单测代码,而 JaCoCo 则是用来生成...

陈树义 752

远程验证神器:Agent Skills Crabbox技能深度解析与实战应用指南 [特殊字符]

**Crabbox远程验证技能**是OpenClaw Agent Skills项目中的核心组件,专门为开发者和AI助手提供强大的远程环境验证能力。这个技能让您能够在云端Linux环境中执行复杂的测试、CI验证和部署检查,确保代码在不同环境中的一致性。 ## 为什么需要远程验证技能?🤔 在现代软件开发中,本地环境与生产环境的差异常常导致"在我机器上能运行"的问题。**Crabbox技能**通过

gitblog_00340的博客 994

整车 EOL下线 测试系统介绍

整车下线(EOL)测试专门针对整车进行设计开发,是车企将组装好的整车交付客户前的 最后一道检测工序。EOL 测试针对的是整车下线测试,不同于零部件下线,是整车级别的下线 测试,测试过程关系整车的每个零部件,其测试质量直接影响到整车的质量好坏。因此,整车EOL测试是整车下线必不可少的测试过程.

王小奎的博客 5352

智能体(AI Agent)最常用框架总结.pdf

内容概要:本文系统总结了当前主流的智能体(AI Agent)框架,重点分析了AutoGen、AgentScope、CAMEL和LangGraph四大代表性框架的设计理念、核心机制与适用场景。文章指出,智能体框架通过提供规范化的结构,实现了代码复用、组件解耦、状态管理与可观测性,是构建复杂、可靠智能体应用的必要工具。各框架分别代表了不同的技术路径:AutoGen以“对话驱动协作”为核心,适合角色化多智能体协同;AgentScope强调工程化与分布式能力,适用于高并发、高可靠性的生产环境;CAMEL通过“角色扮演”和“引导性提示”实现轻量级自主协作;LangGraph则将工作流建模为图结构,支持循环与条件跳转,适合需要精确控制的复杂流程。; 适合人群:具备一定AI与编程基础,从事智能体系统开发、研究或应用的相关技术人员,尤其是希望从脚本式开发转向框架化、工程化构建智能体的研发人员;; 使用场景及目标:①理解主流智能体框架的设计哲学与核心技术差异;②根据具体任务需求(如协作模式、流程控制、系统规模、可靠性要求)进行合理选型;③掌握多智能体系统中的状态管理、消息传递、工作流编排等关键实现机制;; 阅读建议:本文不仅介绍框架功能,更强调设计思想与权衡(如“涌现式协作”与“显式控制”、“灵活性”与“工程化”),建议结合GitHub案例实践,深入体会不同框架在真实任务中的表现,从而提升智能体系统的设计与开发能力。

上一篇: 单元测试之浅析Mockito mock Kotlin Object类方法
下一篇: 杂谈之树莓派挂载硬盘
穹柏
博客等级 码龄9年 11粉丝 36原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值