资讯专栏INFORMATION COLUMN

浅谈jmockit中mock机制的实现

lijy91 / 684人阅读

摘要:看到的函数返回类型,估计就是在这里实现了字节码的转换,然后返回了新的被掉包的文件了。虽然是门静态类型语言,不过幸亏有字节码和作为中间层,使得实现起来相对容易。

最近在工作中写单元测试的时候,有使用到jmockit来mock无关对象。

在jmockit中,你可以使用MockUp来创建一个“fake”的实例,对某个方法指定自己的实现,而不是调用实际的方法。

对于接口类型,需要这样调用:

@Mocked
private SomeInterface mockInstance;

mockInstance = new MockUp() {
    ...
}.getMockInstance();

这个倒没有什么古怪的。估计又是使用了java.reflect.Proxy。这个技巧在很多Java框架中用到,比如Spring AOP对于接口类型的实现,就是通过Proxy来混入拦截器实现的。

但是,对于其他类型的调用,就比较奇怪了:

@Mocked
private SomeProxy mockInstance;

new MockUp() {
    @Mock
    public int doSth() {
        return 1;
    }
};

mockInstance.doSth(); // return 1

new出来的对象,如果没有赋值给新的变量,应该是随着GC风飘云散了。可就是在我的眼皮底下,mockInstance就这样被掉包了。

Spring AOP中,对于非接口类型,是通过CGLIB魔改字节码来实现拦截器注入的。所以我估计这个也是一样的道理。不过令人想不通的是,jmockit到底是什么时候进行移花接木的?没看到注入的地方啊……

只能通过看代码来揭秘了。先从MockUp的构造函数开始吧。

// MockUp.java
@Nonnull
private Class redefineClassOrImplementInterface(@Nonnull Class classToMock)
{
  if (classToMock.isInterface()) {
     return createInstanceOfMockedImplementationClass(classToMock, mockedType);
  }

  Class realClass = classToMock;

  if (isAbstract(classToMock.getModifiers())) {
     classToMock = new ConcreteSubclass(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实现起来相对容易。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64488.html

相关文章

  • 静态方法,mock 还是不 mock,这是个问题

    摘要:单元测试中是否要静态方法,一直争论不休,网上有一个一个又一个的讨论,各种意见都有。真要用来静态方法,一般都是结合使用。等工具不支持静态方法,原理上是因为它们都是基于的,只能通过创建子类或实现接口的方式去。什么静态方法构造函数,随时随地想就。 王者 Mockito 不知从何时开始,Mockito 成了 Java 的单元测试框架王者,目前(2019年7月)Github 上 star 数直逼...

    waterc 评论0 收藏0
  • 2021年软件测试工具总结——单元测试工具

    摘要:单元测试框架作为的标准库,是其他单元测试框架的基础。可以和和配合使用编写单元测试。官网地址单元测试覆盖率工具单元测试中还需要用到代码覆盖率工具。代码覆盖率统计工具用来发现没有被测试覆盖的代码,完善单元测试的覆盖率。 在应用程序中,单元是具有一个或多个输入和单个输出的软件中最小可测试部分。单元...

    qingshanli1988 评论0 收藏0
  • 浅谈前端mock

    摘要:引言前端开发经常需要等待后端的接口,严重影响了开发效率,我们一般采用方式来避免这个问题。可能会涉及到门技术,分别是服务端技术随机生成特定格式数据的技术请求转发请求拦截。 引言 前端开发经常需要等待后端的接口,严重影响了开发效率,我们一般采用mock方式来避免这个问题。本人参考了大量文章,结合自己的经验,给出自己在mock上的一些理解。 1. 原理 何为mock,我认为mock主要就是通...

    elina 评论0 收藏0

发表评论

0条评论

lijy91

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<