1

最近在工作中写单元测试的时候,有使用到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实现起来相对容易。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.