[Java] 从 class 文件看 EasyMock 对 @Mock 注解的处理

背景

使用 EasyMock 框架写单元测试时,会用到 @Mock/@TestSubject 这样的注解,那么 EasyMock 框架遇到 @Mock 注解时,做了什么呢?我们一起来探索吧。

要点

代码

铺垫

The Elements of Computing Systems 一书的附录 A 中提到

任何布尔函数都可以用只包含 Nand 运算符的表达式

据此,我们可以写一个小项目,在这个项目里,我们用 Nand 来实现 Not。假设我们还没有实现 Nand 的逻辑,此时只好先通过 mock 的方式来进行测试。

项目结构

我们在项目顶层执行 tree . 命令,会看到如下的结果 ⬇️

text 复制代码
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── org
    │           └── example
    │               ├── NandGate.java
    │               └── NotGate.java
    └── test
        └── java
            └── org
                └── example
                    └── NotGateTest.java

10 directories, 4 files

项目里共有 4 个文件

  • pom.xml
  • NandGate.java
  • NotGate.java
  • NotGateTest.java

它们的内容如下

pom.xml

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

java 复制代码
package org.example;

public class NandGate {
    public boolean calc(boolean a, boolean b) {
        // TODO 假设这里的逻辑还没有实现
        return false;
    }
}

我们假设 NandGate 的逻辑还没有实现

NotGate.java

java 复制代码
package org.example;

public class NotGate {
    private NandGate nandGate;

    public boolean calc(boolean in) {
        return nandGate.calc(in, in);
    }
}

NotGateTest.java

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)⬇️

分析

注意到 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NotGateTest \text{org.example.NotGateTest} </math>org.example.NotGateTest 带有 @RunWith(EasyMockRunner.class) 注解,我们可以从 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.easymock.EasyMockRunner \text{org.easymock.EasyMockRunner} </math>org.easymock.EasyMockRunner 入手。它的简要类图如下 ⬇️

在下图所示的位置,看起来会处理 @Mock 注解 (以及 @TestSubject 注解)

经过一番查找,我发现了 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.easymock.internal.ClassProxyFactory \text{org.easymock.internal.ClassProxyFactory} </math>org.easymock.internal.ClassProxyFactory 类中 doCreateProxy(Class<T>, InvocationHandler, ClassInfoProvider, Method[], ConstructorArgs) 方法,在这个方法里会用 Byte Buddy 生成代理类 ⬇️

我们在 return 语句这里打一个断点(断点的位置如下图所示),并将这个断点简称为 断点甲

运行 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NotGateTest \text{org.example.NotGateTest} </math>org.example.NotGateTest 中的测试,当程序运行到 断点甲 这里时,我们在 Threads & Variables 这个标签页的输入框里输入如下内容(这是为了将动态代理类以 class 文件的形式保存下来)

java 复制代码
unloaded.saveIn(new java.io.File("."))

按回车键执行它。一直点击表示 Resume Program 的那个绿色按钮(其位置如下图所示),让程序运行完。

此时会看到,在项目顶层多了一个 org 目录。执行 tree org 命令后,会看到如下的结果(在您的电脑上,可能会看到 不同名称class 文件)

text 复制代码
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 了 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NandGate \text{org.example.NandGate} </math>org.example.NandGate。另外几个 class 文件看起来是起辅助作用的,但不知道它们的具体作用是什么 🤷。先继续看吧。

Intellij IDEA (Community Edition) 反编译 org.example.NandGate$$$EasyMock$1 的结果如下 ⬇️

java 复制代码
//
// 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 了 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.Object \text{java.lang.Object} </math>java.lang.Object 中对应的方法,最后一个方法 override 了 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NandGate \text{org.example.NandGate} </math>org.example.NandGate 中对应的方法。它们的处理逻辑是类似的。我们着重看一个就行了。

我们以 calc(boolean, boolean) 方法为例,细看一下它做了什么。

calc(boolean, boolean) 方法的内容如下 ⬇️ (为了方便阅读,我把每个参数放在在独立的行了)

java 复制代码
// 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 个参数都是什么

  1. this: 当前代理类对象
  2. this.$callback: 类型是 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.easymock.internal.ClassMockingData \text{org.easymock.internal.ClassMockingData} </math>org.easymock.internal.ClassMockingData,从名字来看和 mock 数据相关(本文不关心其中的细节)
  3. cachedValue$vqR1NYOW$r52ujm3: 类型是 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.reflect.Method \text{java.lang.reflect.Method} </math>java.lang.reflect.Method
    • 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NandGate \text{org.example.NandGate} </math>org.example.NandGate 中的 calc(boolean, boolean) 方法对应
  4. new Object[]{var1, var2}: 容纳 calc(boolean, boolean) 方法入参的数组
  5. new NandGate$$$EasyMock$1$auxiliary$EJofXKO9(this, var1, var2): 这个有点复杂,下文细说

只有第 5 个参数看起来比较复杂。我给 NandGate$$$EasyMock$1$auxiliary$EJofXKO9 画了张类图 ⬇️ (请注意,在您的电脑上,这个类可能会是其他的名称)

NandGate$$$EasyMock$1$auxiliary$EJofXKO9 里有下列 3 个字段

  • argument0: 保存 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.NandGate \text{org.example.NandGate} </math>org.example.NandGate 的动态代理类(即,NandGate$$$EasyMock$1)的实例
  • argument1: 保存 calc(boolean var1, boolean var2) 方法入参中的 var1
  • argument2: 保存 calc(boolean var1, boolean var2) 方法入参中的 var2

NandGate$$$EasyMock$1$auxiliary$EJofXKO9call()run() 方法中都会调用 NandGate$$$EasyMock$1calc$accessor$vqR1NYOW(boolean, boolean) 方法。后者的内容如下(由 Intellij IDEA (Community Edition) 反编译)

java 复制代码
// 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) 方法的入口。

我们再看看 <math xmlns="http://www.w3.org/1998/Math/MathML"> org.easymock.internal.ClassProxyFactory.MockMethodInterceptor \text{org.easymock.internal.ClassProxyFactory.MockMethodInterceptor} </math>org.easymock.internal.ClassProxyFactory.MockMethodInterceptor 中的 interceptSuperCallable(Object, ClassMockingData, Method, Object[], Callable<?>) 方法

在上图红色箭头位置,可以看到有一个 if 语句块。看起来是这样的 ⬇️

  • 如果有 mock 的需要,则由 mockingData.handler() 做相关处理
  • 如果没有 mock 的需要,则调用 superCall.call() (这里的 superCallNandGate$$$EasyMock$1$auxiliary$EJofXKO9 的实例)

小总结

  • EasyMock 框架会为带有 @Mock 注解的字段(假设其类型为 <math xmlns="http://www.w3.org/1998/Math/MathML"> T \text{T} </math>T)生成动态代理类 <math xmlns="http://www.w3.org/1998/Math/MathML"> M \text{M} </math>M(用 Byte Buddy 来生成)
  • 代理类 <math xmlns="http://www.w3.org/1998/Math/MathML"> M \text{M} </math>M 中会合成一些方法,以便调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> T \text{T} </math>T 自身的方法
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> org.easymock.internal.ClassProxyFactory.MockMethodInterceptor \text{org.easymock.internal.ClassProxyFactory.MockMethodInterceptor} </math>org.easymock.internal.ClassProxyFactory.MockMethodInterceptor 中的 interceptSuperCallable(Object, ClassMockingData, Method, Object[], Callable<?>) 方法会根据是否需要 mock,而区别处理
    • 如果不需要 mock,则通过辅助类调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> M \text{M} </math>M 中对应的合成方法(从而间接调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> T \text{T} </math>T 中的方法)
    • 如果需要 mock,则执行 mockingData.handler().invoke(obj, method, args) 这样的逻辑(本文不涉及其中的细节)

其他

画 "要点" 用到的代码

我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️

puml 复制代码
@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 的插件来画那张图,所用到的代码如下 ⬇️

puml 复制代码
@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 的插件来画那张图,所用到的代码如下 ⬇️

puml 复制代码
@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$$EasyMock1 的类图" 用到的代码

我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️

puml 复制代码
@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$$EasyMock 1 1 1auxiliaryEJofXKO9 的类图" 用到的代码

我用了 PlantUML 的插件来画那张图,所用到的代码如下 ⬇️

puml 复制代码
@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

参考资料

相关推荐
小谢小哥2 小时前
06-Java语言核心-JVM原理-JVM内存区域详解
后端
呆子也有梦2 小时前
redis 的延时双删、双重检查锁定在游戏服务端的使用(伪代码为C#)
redis·后端·游戏·缓存·c#
烛之武2 小时前
SpringBoot 实战篇
java·spring boot·后端
lclcooky2 小时前
Spring 核心技术解析【纯干货版】- XII:Spring 数据访问模块 Spring-R2dbc 模块精讲
java·后端·spring
神奇小汤圆2 小时前
Java 集合容器 - 高级篇
后端
Boop_wu2 小时前
[Java EE 进阶] Spring Boot 日志全面解析 : 配置与实战
junit·java-ee·单元测试
祭曦念3 小时前
学Rust3次都放弃?这篇文章帮你避开90%的新手劝退
后端
iPadiPhone3 小时前
万亿级流量的基石:Kafka 核心原理、大厂面试题解析与实战
分布式·后端·面试·kafka