最近在工作中写单元测试的时候,有使用到jmockit来mock无关对象。
在jmockit中,你可以使用MockUp
来创建一个“fake”的实例,对某个方法指定自己的实现,而不是调用实际的方法。
对于接口类型,需要这样调用:
@Mocked
private SomeInterface mockInstance;
mockInstance = new MockUp<SomeInteface>() {
...
}.getMockInstance();
这个倒没有什么古怪的。估计又是使用了java.reflect.Proxy。这个技巧在很多Java框架中用到,比如Spring AOP对于接口类型的实现,就是通过Proxy来混入拦截器实现的。
但是,对于其他类型的调用,就比较奇怪了:
@Mocked
private SomeProxy mockInstance;
new MockUp<SomeProxy>() {
@Mock
public int doSth() {
return 1;
}
};
mockInstance.doSth(); // return 1
new出来的对象,如果没有赋值给新的变量,应该是随着GC风飘云散了。可就是在我的眼皮底下,mockInstance就这样被掉包了。
Spring AOP中,对于非接口类型,是通过CGLIB魔改字节码来实现拦截器注入的。所以我估计这个也是一样的道理。不过令人想不通的是,jmockit到底是什么时候进行移花接木的?没看到注入的地方啊……
只能通过看代码来揭秘了。先从MockUp的构造函数开始吧。
// MockUp.java
@Nonnull
private Class<T> redefineClassOrImplementInterface(@Nonnull Class<T> classToMock)
{
if (classToMock.isInterface()) {
return createInstanceOfMockedImplementationClass(classToMock, mockedType);
}
Class<T> realClass = classToMock;
if (isAbstract(classToMock.getModifiers())) {
classToMock = new ConcreteSubclass<T>(classToMock).generateClass();
}
classesToRestore = redefineMethods(realClass, classToMock, mockedType);
return classToMock;
}
对于非接口类型,调用了redefineMethods
来定义一个仿版。顺着redefineMethods
找下去,终于发现了jmockit的“作案手法”。
// MockClassSetup.java
@Nullable
private byte[] modifyRealClass(@Nonnull Class<?> classToModify)
{
if (rcReader == null) {
rcReader = createClassReaderForRealClass(classToModify);
}
MockupsModifier modifier = new MockupsModifier(rcReader, classToModify, mockUp, mockMethods);
rcReader.accept(modifier, SKIP_FRAMES);
return modifier.wasModified() ? modifier.toByteArray() : null;
}
看到byte[]
的函数返回类型,估计就是在这里实现了字节码的转换,然后返回了新的被掉包的class文件了。沿着MockupsModifier
看下去,可以看到jmockit是用ASM来改动原来的实现(具体见external.asm
这个包,我就没有细看了)。
众所周知,Java代码先是编译成class文件,然后由JVM加载运行的。围绕JVM这一中间层,各种有趣的技术应运而生。比如各种类加载器,可以动态地去加载同名的类的不同实现(不同的class文件)。还有各种魔改class文件的手段,在原来的实现中注入自己的代码,像ASM、javassist、GCLIB,等等。jmockit就是应用ASM来修改原来的class文件,用mocked的实现掉包原来的代码。因为MockUp的构造已经触发了“狸猫换太子”的幕后行为,所以这里就不用把new出来的东西赋值给具体变量了。
还有一个问题。我们虽然弄明白了jmockit的作案手法,可是还没有找到掉包现场呢!即使现在jmockit已经持有了被篡改后的字节码,可它又是怎么替换呢?
继续看下去,发现jmockit把修改后的字节码存在StartUp.java
里面了。转过去会看到,jmockit这里用到了JDK6的一个新特性:动态Instrumentation。怪不得jmockit要求JDK版本知识在6以上。
关于动态Instrumentation,具体可以看下这篇文章:http://www.ibm.com/developerworks/cn/java/j-lo-jse61/
简单来说,通过这一机制可以实现监听JVM加载类的事件,并在此之前运行自己的挂钩方法。这么一来,掉包现场也找到了。
那jmockit怎么知道要监听哪些类呢?前面可以看到,需要Mock的类上,要添加Mocked注解。所以jmockit编写了一些跟主流测试框架集成的代码,在测试运行的时候获取带该注解的类。这样就知道要监听的目标了。
总结一下:jmockit先通过Mocked注解标记需要Mock掉的类。然后调用new MockUp
去创建修改后的class文件。在JVM运行的时候,通过JDK6之后的动态Instrumentation特性监听类加载事件,并在目标类加载之前移花接木,用魔改后的字节码换掉真货。虽然Java是门静态类型语言,不过幸亏有字节码和JVM作为中间层,使得mock实现起来相对容易。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。