如何不借助Gradle/Maven插件以编程方式使用Jacoco与JUnit
解决方案
存在完全符合需求的编程实现方式,你可以通过JaCoCo核心API结合JUnit 5的编程式启动器实现动态传入测试类、待统计覆盖率类列表的效果,不需要提前配置Maven/Gradle插件,完全可以在IJava内核的单元格中即时运行。
你之前的扩展无法运行的核心原因是:Jupyter进程启动时已经完成了JVM初始化,无法再动态挂载JaCoCo代理,常规的挂载代理方案不适用该场景,需要改用JaCoCo的离线插桩+运行时收集执行数据的逻辑。
可直接在IJava内核运行的实现步骤
1. 导入依赖
先在单元格中执行以下代码引入所需依赖:
%maven org.jacoco:org.jacoco.core:0.8.11 %maven org.jacoco:org.jacoco.report:0.8.11 %maven org.junit.jupiter:junit-jupiter-api:5.9.2 %maven org.junit.jupiter:junit-jupiter-engine:5.9.2 %maven org.junit.platform:junit-platform-launcher:1.9.2
2. 核心实现代码
以下封装的工具方法可以直接传入测试类列表和待统计覆盖率的类名列表,执行后返回覆盖率统计结果:
import org.jacoco.core.instr.Instrumenter; import org.jacoco.core.runtime.IRuntime; import org.jacoco.core.runtime.LoggerRuntime; import org.jacoco.core.runtime.RuntimeData; import org.jacoco.core.data.ExecutionDataStore; import org.jacoco.core.data.SessionInfoStore; import org.junit.platform.launcher.core.LauncherFactory; import org.junit.platform.launcher.listeners.SummaryGeneratingListener; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.HashMap; public class JacocoJunitRunner { private static final IRuntime runtime = new LoggerRuntime(); public static Map<String, Object> run(List<Class<?>> testClasses, List<String> targetClassNames) throws Exception { // 1. 初始化JaCoCo运行时 RuntimeData data = new RuntimeData(); runtime.startup(data); // 2. 对待统计覆盖率的类进行插桩 Instrumenter instrumenter = new Instrumenter(runtime); Map<String, Class<?>> instrumentedClasses = new HashMap<>(); for (String className : targetClassNames) { String classResource = "/" + className.replace('.', '/') + ".class"; byte[] originalBytes = JacocoJunitRunner.class.getResourceAsStream(classResource).readAllBytes(); byte[] instrumentedBytes = instrumenter.instrument(originalBytes, className); // 自定义加载插桩后的类 Class<?> instrumentedClass = new ClassLoader() { public Class<?> defineClass(String name, byte[] bytes) { return defineClass(name, bytes, 0, bytes.length); } }.defineClass(className, instrumentedBytes); instrumentedClasses.put(className, instrumentedClass); } // 3. 配置并执行JUnit测试 SummaryGeneratingListener listener = new SummaryGeneratingListener(); var requestBuilder = LauncherDiscoveryRequestBuilder.request(); testClasses.forEach(clazz -> requestBuilder.selectors(selectClass(clazz))); var launcher = LauncherFactory.create(); launcher.registerTestExecutionListeners(listener); launcher.execute(requestBuilder.build()); // 4. 收集覆盖率数据 ExecutionDataStore executionData = new ExecutionDataStore(); SessionInfoStore sessionInfo = new SessionInfoStore(); data.collect(executionData, sessionInfo, false); runtime.shutdown(); // 封装返回结果 Map<String, Object> result = new HashMap<>(); result.put("testSummary", listener.getSummary()); result.put("coverageData", executionData); return result; } }
3. 使用示例
// 假设你已经定义了业务类com.example.Demo和测试类com.example.DemoTest var result = JacocoJunitRunner.run( List.of(com.example.DemoTest.class), List.of("com.example.Demo") ); // 打印测试结果 System.out.println("测试用例执行结果:" + result.get("testSummary").toString()); // 可自行扩展代码将coverageData转为可读的行覆盖率、分支覆盖率数值
原始问题复现步骤
如果需要复现原始问题的运行环境,可以按以下步骤操作:
- 使用docker运行:
docker run -v notebook:/server/notebook -p 8888:8888 nicolalandro/multilanguage-jupyter:1.0 - 也可选择对应仓库README中的部署按钮,将Jupyter notebook部署到Heroku上
- 下载并打开对应测试笔记本即可复现问题
内容的提问来源于stack exchange,提问作者Nicola Landro
相关产品推荐
相关产品推荐

