背景
使用 EasyMock 框架写单元测试时,会用到 @Mock/@TestSubject 这样的注解,那么 EasyMock 框架遇到 @Mock 注解时,做了什么呢?我们一起来探索吧。
要点
代码
铺垫
The Elements of Computing Systems 一书的附录 A 中提到
任何布尔函数都可以用只包含
Nand运算符的表达式
据此,我们可以写一个小项目,在这个项目里,我们用 Nand 来实现 Not。假设我们还没有实现 Nand 的逻辑,此时只好先通过 mock 的方式来进行测试。
项目结构
我们在项目顶层执行 tree . 命令,会看到如下的结果 ⬇️
.
├── pom.xml
└── src
├── main
│ └── java
│ └── org
│ └── example
│ ├── NandGate.java
│ └── NotGate.java
└── test
└── java
└── org
└── example
└── NotGateTest.java
10 directories, 4 files
项目里共有 4 个文件
pom.xmlNandGate.javaNotGate.javaNotGateTest.java
它们的内容如下
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>easy-mock-study</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>5.6.0</version>
</dependency>
</dependencies>
</project>
NandGate.java
package org.example;
public class NandGate {
public boolean calc(boolean a, boolean b) {
// TODO 假设这里的逻辑还没有实现
return false;
}
}
我们假设 NandGate 的逻辑还没有实现
NotGate.java
package org.example;
public class NotGate {
private NandGate nandGate;
public boolean calc(boolean in) {
return nandGate.calc(in, in);
}
}
NotGateTest.java
package org.example;
import org.easymock.*;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(EasyMockRunner.class)
public class NotGateTest {
@TestSubject
private NotGate notGate;
@Mock
private NandGate nandGate;
@Test
public void testCalc() {
EasyMock.expect(nandGate.calc(true, true)).andReturn(false);
EasyMock.expect(nandGate.calc(false, false)).andReturn(true);
EasyMock.replay(nandGate);
Assert.assertFalse(notGate.calc(true));
Assert.assertTrue(notGate.calc(false));
EasyMock.verify(nandGate);
}
}
由于 NandGate 的逻辑还没有实现,而 NotGate 又依赖了 NandGate,所以在对 NotGate 进行单元测试时,需要 mock NandGate 的行为。
项目中定义了 3 个类,它们的类图如下 ⬇️
运行
运行 NotGateTest 中的测试,没有出现异常 (但有一些 warning,本文不关心这些 warning)⬇️
分析
注意到 带有 @RunWith(EasyMockRunner.class) 注解,我们可以从 入手。它的简要类图如下 ⬇️
在下图所示的位置,看起来会处理 @Mock 注解 (以及 @TestSubject 注解)
经过一番查找,我发现了 类中的 doCreateProxy(Class<T>, InvocationHandler, ClassInfoProvider, Method[], ConstructorArgs) 方法,在这个方法里会用 Byte Buddy 生成代理类 ⬇️
我们在 return 语句这里打一个断点(断点的位置如下图所示),并将这个断点简称为 断点甲。
运行 中的测试,当程序运行到 断点甲 这里时,我们在 Threads & Variables 这个标签页的输入框里输入如下内容(这是为了将动态代理类以 class 文件的形式保存下来)
unloaded.saveIn(new java.io.File("."))
按回车键执行它。一直点击表示 Resume Program 的那个绿色按钮(其位置如下图所示),让程序运行完。
此时会看到,在项目顶层多了一个 org 目录。执行 tree org 命令后,会看到如下的结果(在您的电脑上,可能会看到 不同名称 的 class 文件)
org
└── example
├── NandGate$$$EasyMock$1.class
├── NandGate$$$EasyMock$1$auxiliary$3iDMERZ7.class
├── NandGate$$$EasyMock$1$auxiliary$bHJHIKPG.class
├── NandGate$$$EasyMock$1$auxiliary$EJofXKO9.class
├── NandGate$$$EasyMock$1$auxiliary$rXtS8upH.class
└── NandGate$$$EasyMock$1$auxiliary$yqnHxFk3.class
2 directories, 6 files
我在 Intellij IDEA (Community Edition) 里看了看,只有 org.example.NandGate$$$EasyMock$1 extend 了 。另外 5 个 class 文件看起来是起辅助作用的,但不知道它们的具体作用是什么 🤷。先继续看吧。
Intellij IDEA (Community Edition) 反编译 org.example.NandGate$$$EasyMock$1 的结果如下 ⬇️
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.example;
import java.lang.reflect.Method;
import org.easymock.internal.ClassMockingData;
import org.easymock.internal.ClassProxyFactory.MockMethodInterceptor;
public class NandGate$$$EasyMock$1 extends NandGate {
// $FF: synthetic field
public ClassMockingData $callback;
// $FF: synthetic field
private static final Method cachedValue$vqR1NYOW$4cscpe1 = Object.class.getMethod("toString");
// $FF: synthetic field
private static final Method cachedValue$vqR1NYOW$r52ujm3;
// $FF: synthetic field
private static final Method cachedValue$vqR1NYOW$5j4bem0;
// $FF: synthetic field
private static final Method cachedValue$vqR1NYOW$7m9oaq0;
// $FF: synthetic field
private static final Method cachedValue$vqR1NYOW$9pqdof1;
public boolean equals(Object var1) {
return (Boolean)MockMethodInterceptor.interceptSuperCallable(this, this.$callback, cachedValue$vqR1NYOW$5j4bem0, new Object[]{var1}, new NandGate$$$EasyMock$1$auxiliary$3iDMERZ7(this, var1));
}
public String toString() {
return (String)MockMethodInterceptor.interceptSuperCallable(this, this.$callback, cachedValue$vqR1NYOW$4cscpe1, new Object[0], new NandGate$$$EasyMock$1$auxiliary$bHJHIKPG(this));
}
public int hashCode() {
return (Integer)MockMethodInterceptor.interceptSuperCallable(this, this.$callback, cachedValue$vqR1NYOW$9pqdof1, new Object[0], new NandGate$$$EasyMock$1$auxiliary$yqnHxFk3(this));
}
protected Object clone() throws CloneNotSupportedException {
return MockMethodInterceptor.interceptSuperCallable(this, this.$callback, cachedValue$vqR1NYOW$7m9oaq0, new Object[0], new NandGate$$$EasyMock$1$auxiliary$rXtS8upH(this));
}
public boolean calc(boolean var1, boolean var2) {
return (Boolean)MockMethodInterceptor.interceptSuperCallable(this, this.$callback, cachedValue$vqR1NYOW$r52ujm3, new Object[]{var1, var2}, new NandGate$$$EasyMock$1$auxiliary$EJofXKO9(this, var1, var2));
}
public NandGate$$$EasyMock$1() {
super();
}
static {
cachedValue$vqR1NYOW$r52ujm3 = NandGate.class.getMethod("calc", Boolean.TYPE, Boolean.TYPE);
cachedValue$vqR1NYOW$5j4bem0 = Object.class.getMethod("equals", Object.class);
cachedValue$vqR1NYOW$7m9oaq0 = Object.class.getDeclaredMethod("clone");
cachedValue$vqR1NYOW$9pqdof1 = Object.class.getMethod("hashCode");
}
// $FF: synthetic method
final int hashCode$accessor$vqR1NYOW() {
return super.hashCode();
}
// $FF: synthetic method
final Object clone$accessor$vqR1NYOW() throws CloneNotSupportedException {
return super.clone();
}
// $FF: synthetic method
final String toString$accessor$vqR1NYOW() {
return super.toString();
}
// $FF: synthetic method
final boolean calc$accessor$vqR1NYOW(boolean var1, boolean var2) {
return super.calc(var1, var2);
}
// $FF: synthetic method
final boolean equals$accessor$vqR1NYOW(Object var1) {
return super.equals(var1);
}
}
基于上述结果,我给 org.example.NandGate$$$EasyMock$1 画了张类图 ⬇️ (我把合成字段和合成方法也画在类图中了)
除去合成方法和构造函数,org.example.NandGate$$$EasyMock$1 共有下列的 5 个方法
equals(Object)toString()hashCode()clone()calc(boolean, boolean)
前 4 个方法都 override 了 中对应的方法,最后一个方法 override 了 中对应的方法。它们的处理逻辑是类似的。我们着重看一个就行了。
我们以 calc(boolean, boolean) 方法为例,细看一下它做了什么。
calc(boolean, boolean) 方法的内容如下 ⬇️ (为了方便阅读,我把每个参数放在在独立的行了)
// org.example.NandGate$$$EasyMock$1 中的 calc(boolean, boolean) 方法
public boolean calc(boolean var1, boolean var2) {
return (Boolean)MockMethodInterceptor.interceptSuperCallable(
this,
this.$callback,
cachedValue$vqR1NYOW$r52ujm3,
new Object[]{var1, var2},
new NandGate$$$EasyMock$1$auxiliary$EJofXKO9(this, var1, var2)
);
}
我们看看这 5 个参数都是什么
this: 当前代理类对象this.$callback: 类型是 ,从名字来看和mock数据相关(本文不关心其中的细节)cachedValue$vqR1NYOW$r52ujm3: 类型是- 和 中的
calc(boolean, boolean)方法对应
- 和 中的
new Object[]{var1, var2}: 容纳calc(boolean, boolean)方法入参的数组new NandGate$$$EasyMock$1$auxiliary$EJofXKO9(this, var1, var2): 这个有点复杂,下文细说
只有第 5 个参数看起来比较复杂。我给 NandGate$$$EasyMock$1$auxiliary$EJofXKO9 画了张类图 ⬇️ (请注意,在您的电脑上,这个类可能会是其他的名称)
NandGate$$$EasyMock$1$auxiliary$EJofXKO9 里有下列 3 个字段
argument0: 保存 的动态代理类(即,NandGate$$$EasyMock$1)的实例argument1: 保存calc(boolean var1, boolean var2)方法入参中的var1argument2: 保存calc(boolean var1, boolean var2)方法入参中的var2
在 NandGate$$$EasyMock$1$auxiliary$EJofXKO9 的 call() 和 run() 方法中都会调用
NandGate$$$EasyMock$1 的 calc$accessor$vqR1NYOW(boolean, boolean) 方法。后者的内容如下(由 Intellij IDEA (Community Edition) 反编译)
// NandGate$$$EasyMock$1 中的 calc$accessor$vqR1NYOW(boolean, boolean) 方法 ⬇️
// $FF: synthetic method
final boolean calc$accessor$vqR1NYOW(boolean var1, boolean var2) {
return super.calc(var1, var2);
}
NandGate$$$EasyMock$1 的继承自 NandGate。由此可以推测,calc$accessor$vqR1NYOW(boolean, boolean) 方法的作用是:提供调用 NandGate 自身的 calc(boolean, boolean) 方法的入口。
我们再看看 中的 interceptSuperCallable(Object, ClassMockingData, Method, Object[], Callable<?>) 方法
在上图红色箭头位置,可以看到有一个 if 语句块。看起来是这样的 ⬇️
- 如果有
mock的需要,则由mockingData.handler()做相关处理 - 如果没有
mock的需要,则调用superCall.call()(这里的superCall是NandGate$$$EasyMock$1$auxiliary$EJofXKO9的实例)
小总结
EasyMock框架会为带有@Mock注解的字段(假设其类型为 )生成动态代理类 (用Byte Buddy来生成)- 代理类 中会合成一些方法,以便调用 自身的方法
- 中的
interceptSuperCallable(Object, ClassMockingData, Method, Object[], Callable<?>)方法会根据是否需要mock,而区别处理- 如果不需要
mock,则通过辅助类调用 中对应的合成方法(从而间接调用 中的方法) - 如果需要
mock,则执行mockingData.handler().invoke(obj, method, args)这样的逻辑(本文不涉及其中的细节)
- 如果不需要
其他
画 "要点" 用到的代码
我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️
@startmindmap
title 要点
*:<i>EasyMock</i> 框架会为带有 <i>@Mock</i> 注解的字段 (假设字段的类型为 <b><i>T</i></b>)
生成动态代理类 <b><i>M</i></b> (用 <i>Byte Buddy</i> 来生成);
* 代理类 <b><i>M</i></b> 中会合成一些方法, 以便调用 <b><i>T</i></b> 自身的方法
*:<i>org.easymock.internal.ClassProxyFactory.MockMethodInterceptor</i> 中的
<i>interceptSuperCallable(Object, ClassMockingData, Method, Object[], Callable<?>)</i> 方法
会根据是否需要 <i>mock</i> 而区别处理;
**:如果不需要 <i>mock</i>, 则通过辅助类调用 <b><i>M</i></b> 中对应的合成方法
(从而间接调用 <b><i>T</i></b> 中的方法);
**:如果需要 <i>mock</i>,
则执行 <i>mockingData.handler().invoke(obj, method, args)</i> 这样的逻辑
(本文不涉及其中的细节);
@endmindmap
画 "类图" 用到的代码
我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title 类图
class org.example.NandGate {
+ boolean calc(boolean, boolean)
}
class org.example.NotGate {
- NandGate nandGate
+ boolean calc(boolean)
}
class org.example.NotGateTest {
- NotGate notGate
- NandGate nandGate
+ void testCalc()
}
note top of org.example.NotGateTest
<i>org.example.NotGateTest</i> 类上有 <i>@RunWith(EasyMockRunner.class)</i> 注解
end note
note left of org.example.NotGateTest::notGate
这个字段带有 <i>@TestSubject</i> 注解
end note
note left of org.example.NotGateTest::nandGate
这个字段带有 <i>@Mock</i> 注解
end note
note left of org.example.NotGateTest::testCalc
这个方法有 <i>@Test</i> 注解
end note
@enduml
画 "EasyMockRunner 的简要类图" 用到的代码
我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>EasyMockRunner</i> 的简要类图
abstract org.junit.runners.ParentRunner<T>
abstract org.junit.runner.Runner
class org.junit.runners.BlockJUnit4ClassRunner
class org.easymock.EasyMockRunner
org.junit.runner.Runner <|-- org.junit.runners.ParentRunner
org.junit.runners.ParentRunner <|-- org.junit.runners.BlockJUnit4ClassRunner : extends ParentRunner<FrameworkMethod>
org.junit.runners.BlockJUnit4ClassRunner <|-- org.easymock.EasyMockRunner
@enduml
画 "org.example.NandGate$$$EasyMock$1 的类图" 用到的代码
我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>org.example.NandGate$$$EasyMock$1</i> 的类图
class org.example.NandGate
class org.example.NandGate$$$EasyMock$1
org.example.NandGate <|-- org.example.NandGate$$$EasyMock$1
class org.example.NandGate {
+ boolean calc(boolean a, boolean b)
}
class org.example.NandGate$$$EasyMock$1 {
+ ClassMockingData $callback;
- {static} final Method cachedValue$vqR1NYOW$4cscpe1
- {static} final Method cachedValue$vqR1NYOW$r52ujm3
- {static} final Method cachedValue$vqR1NYOW$5j4bem0
- {static} final Method cachedValue$vqR1NYOW$7m9oaq0
- {static} final Method cachedValue$vqR1NYOW$9pqdof1
+ boolean equals(Object var1)
+ String toString()
+ int hashCode()
# Object clone() throws CloneNotSupportedException
+ boolean calc(boolean var1, boolean var2)
+ NandGate$$$EasyMock$1()
final int hashCode$accessor$vqR1NYOW()
final Object clone$accessor$vqR1NYOW() throws CloneNotSupportedException
final String toString$accessor$vqR1NYOW()
final boolean calc$accessor$vqR1NYOW(boolean var1, boolean var2)
final boolean equals$accessor$vqR1NYOW(Object var1)
}
@enduml
画 "NandGate$$$EasyMockauxiliary$EJofXKO9 的类图" 用到的代码
我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️
@startuml
'https://plantuml.com/class-diagram
title <i>NandGate$$$EasyMock$1$auxiliary$EJofXKO9</i> 的类图
interface java.lang.Runnable
interface java.util.concurrent.Callable
interface java.io.Serializable
class org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9
java.lang.Runnable <|.. org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9
java.util.concurrent.Callable <|.. org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9
java.io.Serializable <|.. org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9
class org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9 {
- NandGate$$$EasyMock$1 argument0
- boolean argument1
- boolean argument2
+ Object call() throws Exception
+ void run()
NandGate$$$EasyMock$1$auxiliary$EJofXKO9(NandGate$$$EasyMock$1, boolean, boolean)
}
note left of org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9::call
可以认为它对应的 <i>java</i> 代码是这样的 <:point_down:>
<code>
public call() throws Exception {
return Boolean.valueOf(this.argument0.calc$accessor$vqR1NYOW(this.argument1, this.argument2));
}
</code>
end note
note left of org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9::run
可以认为它对应的 <i>java</i> 代码是这样的 <:point_down:>
<code>
public run() {
this.argument0.calc$accessor$vqR1NYOW(this.argument1, this.argument2);
}
</code>
end note
note left of org.example.NandGate$$$EasyMock$1$auxiliary$EJofXKO9::NandGate$$$EasyMock$1$auxiliary$EJofXKO9
可以认为它对应的 <i>java</i> 代码是这样的 <:point_down:>
<code>
NandGate$$$EasyMock$1$auxiliary$EJofXKO9(NandGate$$$EasyMock$1 arg0, boolean arg1, boolean arg2) {
super();
this.argument0 = arg0;
this.argument1 = arg1;
this.argument2 = arg2;
}
</code>
end note
@enduml
参考资料
- EasyMock
- PlantUML 网站 里关于 Class Diagram 的介绍 (中文版的链接是 类图)
- 浅解 JUnit 4 第十七篇:如何实现一个简单的 Runner?