50 简单了解下MockUp

50 简单了解下MockUp前言 呵呵 最近在写单元测试的时候 正在头疼 怎么构造测试用例的数据呢 有些情况实在是比较太难造出来 又或者是造出来 时间成本太大了 还要去做仔细的考虑 还需要单步调试 有些时候这里被过滤掉了 但是实际上我们不期望它被过滤掉 就显得有点麻烦了 呵呵 这时候 突然对 Mock 想起了一些东西 之前 我对于 Mock 的了解局限于 创建一个假的对象

大家好,我是讯享网,很高兴认识大家。

前言

呵呵 最近在写单元测试的时候, 正在头疼 怎么构造测试用例的数据呢?,

有些情况实在是比较太难造出来, 又或者是造出来 时间成本太大了, 还要去做仔细的考虑(还需要单步调试, 有些时候这里被过滤掉了, 但是实际上我们不期望它被过滤掉), 就显得有点麻烦了 

呵呵 这时候 突然对 Mock 想起了一些东西, 之前 我对于 Mock 的了解局限于 创建一个假的对象, 自动填充随机属性啊, 限定特定的方法 返回特定的数据[when(mock.someMethod()).thenReturn(value)] 

呵呵 搜索了一下 项目中使用到 Mock 的地方, 发现了 MockUp 的一种使用的方式, 直接 运行时替换掉某个方法的实现, 呵呵 这下子 写测试用例就更简单方便了, 不用于执着于要测试的代码的细节了 

 

以下代码基于 : jdk1.8.0_211 + jmockit1.49

 

 

测试代码如下

package com.hx.test; import mockit.Mock; import mockit.MockUp; import org.junit.Test; / * MockUp * * @author Jerry.X.He <@.com> * @version 1.0 * @date 2020-03-05 16:04 */ public class Test01MockUp { // Test01MockUp @Test public void test01MockUp() { System.out.println(foo()); new MockUp<Test01MockUp>() { @Mock public String foo() { return "replaced"; } }; System.out.println(foo()); } // foo public static String foo() { return "foo"; } } 

讯享网

单元测试输出如下 


讯享网

 

注 : 启动程序, 需要加上如下vm参数, -javaagent:/Users/jerry/.m2/repository/org/jmockit/jmockit/1.49/jmockit-1.49.jar

呵呵 这个太神奇了吧, 居然 我调用同一个方法, 妈的 返回结果居然不一样, 简直不敢相信自己的眼睛了 

 

 

反编译的Test01MockUp

有一些比较直接的方法, 可以看到 Mock.foo 被执行的情况, 但是 看到的不够明晰, 所以 我们从 反编译之后的字节码来看下吧 

在第一个 Test01MockUp.foo 的地方打上一个断点, 然后 dump 一下 Test01MockUp.class, 拿到的 class 中关键信息如下

讯享网master:javac jerry$ javap -c Test01MockUp_hackBefore.class Compiled from "Test01MockUp.java" public class com.hx.test.Test01MockUp { public com.hx.test.Test01MockUp(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void test01MockUp(); Code: 0: new #2 // class com/hx/test/Test01MockUp$1 3: dup 4: aload_0 5: invokespecial #3 // Method com/hx/test/Test01MockUp$1."<init>":(Lcom/hx/test/Test01MockUp;)V 8: pop 9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 12: invokestatic #5 // Method foo:()Ljava/lang/String; 15: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: return public static java.lang.String foo(); Code: 0: ldc #7 // String foo 2: areturn } 

 

在第二个 Test01MockUp.foo 的地方打上一个断点, 然后 dump 一下 Test01MockUp.class, 拿到的 class 中关键信息如下 

master:javac jerry$ javap -c Test01MockUp_hackAfter.class Compiled from "Test01MockUp.java" public class com.hx.test.Test01MockUp { public com.hx.test.Test01MockUp(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void test01MockUp(); Code: 0: new #2 // class com/hx/test/Test01MockUp$1 3: dup 4: aload_0 5: invokespecial #3 // Method com/hx/test/Test01MockUp$1."<init>":(Lcom/hx/test/Test01MockUp;)V 8: pop 9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 12: invokestatic #5 // Method foo:()Ljava/lang/String; 15: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: return public static java.lang.String foo(); Code: 0: ldc #42 // String com/hx/test/Test01MockUp$1 2: sipush -1 5: invokestatic #48 // Method mockit/internal/state/TestRun.updateFakeState:(Ljava/lang/String;I)Z 8: ifeq 81 11: getstatic #54 // Field java/lang/NegativeArraySizeException.$FMB:Ljava/lang/reflect/InvocationHandler; 14: aconst_null 15: aconst_null 16: sipush 6 19: anewarray #9 // class java/lang/Object 22: dup 23: sipush 0 26: ldc #42 // String com/hx/test/Test01MockUp$1 28: aastore 29: dup 30: sipush 1 33: ldc #55 // String com/hx/test/Test01MockUp 35: aastore 36: dup 37: sipush 2 40: sipush 9 43: invokestatic #61 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 46: aastore 47: dup 48: sipush 3 51: ldc #7 // String foo 53: aastore 54: dup 55: sipush 4 58: ldc #62 // String ()Ljava/lang/String; 60: aastore 61: dup 62: sipush 5 65: sipush -1 68: invokestatic #61 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 71: aastore 72: invokeinterface #68, 4 // InterfaceMethod java/lang/reflect/InvocationHandler.invoke:(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object; 77: checkcast #70 // class java/lang/String 80: areturn 81: ldc #7 // String foo 83: areturn } 

对比一下, 可以发现 第二个断点处的 Test01MockUp.class 的 foo 方法长了很多, 增加了一些 代码片段, 大致如下 

讯享网if(TestRun.updateFakeState("com/hx/test/Test01MockUp$1", -1)) { return (String)NegativeArraySizeException.$FMB.invoke(null, null, new Object[] { "com/hx/test/Test01MockUp$1", "com/hx/test/Test01MockUp", Integer.valueOf(9), "foo", "()Ljava/lang/String;", Integer.valueOf(-1) } ) }

那么 这段代码 是怎么被添加进来的呢 ?

另外一个问题是 NegativeArraySizeException 里面继承自 RuntimeException, 并且没有字段, 那么这里的 $FMB 又是如何添加进来的呢?, 对应的值 又是什么时候初始化的呢 ?

 

 

这部分intercept的代码是怎么添加进来的, class是怎么运行时替换的呢?

在 MockUp 初始化的时候, 会尝试重写 Mock 的类, 这里的 foo 方法会被 Mock 

如上代码 generateCallToUpdateFakeState 里面生成的是 

TestRun.updateFakeState("com/hx/test/Test01MockUp$1", -1)

generateConditionalCallForFakedMethod 里面生成的是 

讯享网if($updateFakeStateReturnVal) { return (String)NegativeArraySizeException.$FMB.invoke(null, null, new Object[] { "com/hx/test/Test01MockUp$1", "com/hx/test/Test01MockUp", Integer.valueOf(9), "foo", "()Ljava/lang/String;", Integer.valueOf(-1) } ) }

 

具体看下一下两个方法实现, generateCallToUpdateFakeState 方法实现如下 

generateConditionalCallForFakedMethod 方法实现如下 

我们这里的 generateCallToFakeMethod 实现如下  

那么这时候 再根据代码的意思 再来对一下 以上部分增加的 拦截的代码呢 ?

 

那么 更改了方法的code之后, 方法所在的类 是如何运行时替换的呢 ?

 

具体的替换运行时的class的api, 是使用的 Instrumentation.redefineClasses 

 

 

具体的代理方法的调用? 

invoke 传递的相关参数如上图, 相关逻辑意义 变量名字也标记的很清楚 

 

再往下的调用链路就很清晰了, 基于反射调用 fakeObject 的同命方法(invoke 对象就是 new MockUp 创建的内部类实例) 

 

 

NegativeArraySizeException.$FMB 哪来的?字段如何初始化 ?

我们查看一下 普通的 java application 里面的 NegativeArraySizeException

master:javac jerry$ javap -c NegativeArraySizeException_hackBefore.class Compiled from "NegativeArraySizeException.java" public class java.lang.NegativeArraySizeException extends java.lang.RuntimeException { public java.lang.NegativeArraySizeException(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/RuntimeException."<init>":()V 4: return public java.lang.NegativeArraySizeException(java.lang.String); Code: 0: aload_0 1: aload_1 2: invokespecial #2 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V 5: return } 

再看下 我们测试用例中的 NegativeArraySizeException

讯享网master:javac jerry$ javap -c NegativeArraySizeException_hackAfter.class Compiled from "NegativeArraySizeException.java" public class java.lang.NegativeArraySizeException extends java.lang.RuntimeException { public static java.lang.reflect.InvocationHandler $MB; public static java.lang.reflect.InvocationHandler $FB; public static java.lang.reflect.InvocationHandler $FMB; public java.lang.NegativeArraySizeException(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/RuntimeException."<init>":()V 4: return public java.lang.NegativeArraySizeException(java.lang.String); Code: 0: aload_0 1: aload_1 2: invokespecial #2 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V 5: return } 

我们会发现 我们这里的 NegativeArraySizeException 居然多了 三个字段, ???, 这是什么情况 这可是 java.lang.NegativeArraySizeException !! 

 

在加载 "java/lang/NegativeArraySizeException" 的时候, 存在注册的 ClassFileTransformer, 向 NegativeArraySizeException 里面增加了三个字段 

改 ClassTransformer 所做的事情如下  

 

点击查看一下 该 ClassTransformer 的具体的使用的地方 

我们发现 原来是在 ClassLoadingBridgeFields. createSyntheticFieldsInJREClassToHoldClassLoadingBridges, 加载 NegativeArraySizeException 就是从这里触发的, 然后这个名字 也取得很符合实际处理的事情 

这里的处理也就是 加载 NegativeArraySizeException 的时候由 FieldAdditionTransformer 预处理一下, 新增了三个字段, 然后 再委托给 classloader 加载 NegativeArraySizeException 

 

那么字段是如何初始化的呢 ?

初始化的值为 具体的类型的单例实例 

 

这个 hack 的真的很过分, 为什么 在 jdk 的类库上面处理, 在自己的 jmockit 相关类上面处理 不好么?, 我认为这是一种 挑衅..(玩笑)

是这里设计好的啊(这就是的"内定") 

 FieldAdditionTransformer fieldAdditionTransformer = new FieldAdditionTransformer(instrumentation); instrumentation.addTransformer(fieldAdditionTransformer); // Loads some JRE classes expected to not be loaded yet. NegativeArraySizeException.class.getName();

 

以上出现了两种处理字节流的"接口" 

第一种是 ClassFileTransformer, 加载字节流之前预处理 

另外一种是 使用 Instrumentation.redefineClasses 

第一种还是挺好理解的, 但是第二种 接触的还不够, 呵呵 后面再看吧  

依赖于 javaagent 机制 

 

 

完 

 

小讯
上一篇 2025-03-18 16:20
下一篇 2025-03-23 14:52

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/125861.html