Apache DolphinScheduler 项目笔记 — 1. 问题定位和排查问题
导语:
Yuanhao Ji 同学是《暑期 2021 - API 模块 UT 测试问题解决》项目负责人,本文记录了他是如何排查和定位问题。
1. 背景
问题
:在部分 UT 中使用 PowerMock
后,导致 Sonar
不能获取对应的测试报告。
大概原因
:PowerMock
模拟 JDK 的静态方法、构造方法、final 方法、私有方法时,需要把使用这些方法的类加入到 @PrepareForTest
注解中,从而导致单元测试覆盖率不被统计。
2. 思路
2.1 明确问题出处,缩小定位范围
DolphinScheduler UT 中使用了 PowerMock
作为单元测试模拟框架,由 JaCoCo
统计各项覆盖率指标,最后由 Sonar
存储和管理覆盖率数据。而问题表现为 —— Sonar
中的某些 UT 覆盖率为 0,但是 Sonar
只是一个代码质量管理平台,覆盖率数值并不是由它得出的,Sonar
只是读取了 JaCoCo
生成的测试报告。那么,很容易看出,这个问题一定与 JaCoCo
脱不了干系。
进一步观察发现,并非所有 UT 的覆盖率都有问题,只用到 JUnit
的 UT 就可以正常获取覆盖率,而所有出现覆盖率问题的 UT 都少不了 PowerMock
的影子。
因此,覆盖率问题一定与 PowerMock
和 JaCoCo
有关。
2.2 搜集资料,寻找相似问题
分析问题之后,我们首先可以借助搜索引擎找到与之相似或相同的问题,通过了解这些问题的背景和解决方案也许可以找到我们问题的关键点。
基于以上分析,以 JaCoCo
、PowerMock
、覆盖率
这三个关键字在 Google、Baidu 等搜索引擎上搜索资料。幸运的是,网上有大量同类问题,并且都与 JaCoCo
、PowerMock
有关。
其中,正如 Code-coverage-with-JaCoCo[1] 中所说 —— There is NO WAY TO USE PowerMock with JaCoCo On-the-fly instrumentation。PowerMock
和 JaCoCo
的 On-the-fly instrumentation
之间存在问题。经验证发现,DolphinScheduler 目前正是使用的这两者结合的方式,因此这应该就是问题的关键所在。
2.3 分析问题出现原因
基于搜索结果和官方文档,分析问题出现原因。
首先,如 JaCoCo Class Ids[2] 中的描述,JaCoCo
基于 CRC64 算法计算得到 64 位的 Class Id,用它来标识 Java 类,在运行时为每个加载的类采样执行数据并存储到 *.exec
文件中,在分析时(例如生成报告),Class Id 用于将分析的类与执行数据相关联。
而出现覆盖率为 0% ,就是因为待测试的类与 JaCoCo
实际采样的类不一致,也即通过 CRC64 算法计算的 Class Id 改变,从而导致待测试类的测试报告中覆盖率始终为 0。
那么,为何会出现这种 Class Id 改变的情况呢?上述页面中也给出了答复 —— 这种情况通常发生在配置 JaCoCo 代理之前配置了另一个 Java Agent,或者特殊的类加载器预处理了这些类文件,例如:Mocking frameworks、Application servers、Persistence frameworks。而 PowerMock
就是一种 Mock 框架,进一步了解其原理得知,JaCoCo
在加载 class 时会事先把统计代码插入到 class 中(即插桩),当执行到某个测试类时,PowerMock
检测到该类使用了@PrepareForTest
注解,就会在加载该类时使用 Javassist
直接从 class 文件中重新读取字节码,导致 JaCoCo
的修改丢失,稍后 JaCoCo
统计的 class 是被修改之后的,而被修改之后的 class 中并没有 JaCoCo
所插入的统计代码,并且 class 文件发生变化后 Class Id 也与加载时不一致。在分析时,测试结果无法关联到待测试类,最终导致覆盖率始终为 0。
如下图所示,Demo 类后面还跟有 MockitoMock
的字样,说明这是一个由 PowerMock
模拟的对象,而 JaCoCo
却错把它以为是最开始时所加载的 Demo 类。
2.4 制定解决方案
JaCoCo
的 offline instrumentation
可以解决此类问题,它的原理是这样的:在测试前先对 class 文件进行插桩,然后生成插过桩的 class 文件或 jar 包,测试插过桩的 class 和 jar 包后,生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
On-the-fly
模式进行代码覆盖分析更方便和简单,无需提前进行字节码插桩,无需考虑 classpath 的设置。
但存在如下情况则不适合 On-the-fly
,需要采用 offline
提前对字节码插桩:
(1)运行环境不支持 java agent。
(2)部署环境不允许设置 JVM 参数。
(3)字节码需要被转换成其他的虚拟机如 Android Dalvik VM。
(4)动态修改字节码过程中和其他 agent 冲突。
(5)无法自定义用户加载类。
而目前问题就是因为动态修改字节码过程中与 PowerMock
冲突,因此必须使用 offline
模式。
[1] https://github.com/powermock/powermock/wiki/Code-coverage-with-JaCoCo
[2] https://www.eclemma.org/jacoco/trunk/doc/classids.html