笔者所在公司由于近期狠抓代码质量,大家狂补ut。中间使用了PowerMock作为Mock工具来实现对于一些static类需要的mock,但是使用Sonar作为覆盖率报告工具时候发现很多的类在使用注解@PrepareForTest之后覆盖率为0。作为一个十分好学的有责任心的程序员对于该情况做了一番调查找到了原因排除了故障。
首先说结论: PowerMock和Sonar使用的覆盖率工具JaCoCo(Java Code Coverage)冲突导致的 。具体而言:
Javassist
工具,该工具在加载类的时候会直接从.class文件中重新读取字节码导致JaCoCo的修改全没了。点击查看PowerMock作者已经做在github上做的 官方解释
: The simplest way to use JaCoCo it is — on-the-fly instrumentation with using JaCoCo Java Agent. In this case a class in modified when it is being loaded . You can just run you application with JaCoCo agent and a code coverage is calculated. This way is used by Eclemma and Intellij Idea. But there is a big issue. PowerMock instruments classes also. Javassist is used to modify classes. The main issue is that Javassist reads classes from disk and all JaCoCo changes are disappeared . As result zero code coverage for classes witch are loaded by PowerMock class loader.
但是还有两点迷惑的地方:1 github官方文档上言之凿凿: But right now there is**NO WAY TO USE**PowerMock with JaCoCo On-the-fly instrumentation
但是根据笔者使用实际只要没有加入到@PerpareForTest中和JaCoCo配合使用是没有问题的;2在Jacoco官网上还找到另外一种解释并且在笔者Jenkins上也看到了类似的报错: For report generation the same class files must be used as at runtime
。
关于第一点:经过一番对于PowerMock源代码的分析可知只有在而在@PrepareForTest中的类才会使用 Javassist
加载类的字节码:
MockClassLoaderFactory创建这个ClassLoader过程中会将@PrepareForTest的value中的类加入到ClassLoader中去:
public ClassLoader createForClass(final MockTransformer... extraMockTransformer) { final ByteCodeFramework byteCodeFramework = ByteCodeFramework.getByteCodeFrameworkForTestClass(testClass); if (testClass.isAnnotationPresent(PrepareEverythingForTest.class)) { return create(byteCodeFramework, new String[]{MockClassLoader.MODIFY_ALL_CLASSES}, extraMockTransformer); } else { final String[] prepareForTestClasses = prepareForTestExtractor.getTestClasses(testClass); final String[] suppressStaticClasses = suppressionExtractor.getTestClasses(testClass); return create(byteCodeFramework, arrayMerger.mergeArrays(String.class, prepareForTestClasses, suppressStaticClasses), extraMockTransformer); } }
而ClassLoader加载过程中会判断是否需要Mock也就是否在@PrepareForTest的类的列表中,如果再的话就使用LoadMockClass方法加载,这个里面就用到了上文说的 Javassist
:
@Override protected Class<?> loadClassByThisClassLoader(String className) throws ClassFormatError, ClassNotFoundException { final Class<?> loadedClass; Class<?> deferClass = deferTo.loadClass(className); if (getConfiguration().shouldMockClass(className)) { loadedClass = loadMockClass(className, deferClass.getProtectionDomain()); } else { loadedClass = loadUnmockedClass(className, deferClass.getProtectionDomain()); } return loadedClass; }
The root of the problem: "JaCoCo uses a hashcode of the class definition for class identity."
So the cglib modified classes has different hashcodes than the originals.
I don't think the PowerMock guys can solve this problem, so I think this will be a long-term problem.
题外话:
关于 Javassist
加载的问题在GitHub上大神 thekingn0thing
试图使用ByteBuddy来替代Javassist并已经有了部分实现不过最后由于ByteBuddy还不完善很多 Javassist
的功能在ByteBuddy上没能得到很好支持,thekingn0thing不得不自己实现,导致issue过多而又 revert
了: 并表示短期内不会重拾这项工作了
。最后想说开源工作这真是不易,为了让大家使用的方便凭着一份热爱要花费大量的个人业余时间来维护一个免费的工程。
参考:
https://github.com/powermock/powermock/issues/422
https://github.com/powermock/powermock/wiki/Code-coverage-with-JaCoCo
https://github.com/powermock/powermock/issues/727