常用的 Mock Toolkit 有 EasyMock、JMockit、Mockito、PowerMock 等,而本文主要介绍 Mockito 的简单使用,并对其实现原理进行分析。
1. 工作原理
Mockito 的使用方法如下,具体使用方法参考 Unit tests with Mockito - Tutorial[1]:
@Test
public void testCallable() throws Exception {
Callable mock = mock(Callable.class);
when(mock.call()).thenReturn("first");
Assert.assertEquals("first", mock.call());
verify(mock, times(1)).call();
}
1.1 代理阶段
下面我们从 Mockito.mock(Callable.class) 生成代理类开始进行分析。Mockito 使用 Byte Buddy[1] 框架生成动态代理类,因此我们需要先将代理类进行反编译才能看到源码。这里使用阿里开源 Java 应用诊断工具 Arthas[2] 反编译代理类,结果如下:
/**
* Arthas 反编译步骤:
* 1. 启动 Arthas
* java -jar arthas-boot.jar
*
* 2. 输入编号选择进程
* Arthas 启动后,会打印 Java 应用进程列表,如下:
* [1]: 5376
* [2]: 7232 com.intellij.rt.execution.junit.JUnitStarter
* [3]: 4692 org.gradle.launcher.daemon.bootstrap.GradleDaemon
* [4]: 13000 org.jetbrains.jps.cmdline.Launcher
* 这里输入编号 2,让 Arthas 关联到测试类的 Java 进程上
*
* 3. 使用 sc 命令搜索这个Callable接口对应的类名
* $ sc org.mockito.codegen.Callable*
* org.mockito.codegen.Callable$MockitoMock$1568092145
* org.mockito.codegen.Callable$MockitoMock$1568092145$auxiliary$0leHbnY4
* org.mockito.codegen.Callable$MockitoMock$1568092145$auxiliary$ce7uzpM8
*
* 4. 使用 jad 命令反编译 org.mockito.codegen.Callable$MockitoMock$1568092145
* $ jad org.mockito.codegen.Callable$MockitoMock$1568092145
*
* 更多使用方法请参考 Arthas 官方文档:
* https://alibaba.github.io/arthas/quick-start.html
*/
package org.mockito.codegen;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import org.mockito.internal.creation.bytebuddy.MockAccess;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor;
@FunctionalInterface
public class Callable$MockitoMock$1568092145 implements Callable, MockAccess {
private static final long serialVersionUID = 42L;
private MockMethodInterceptor mockitoInterceptor;
private static final /* synthetic */ Method cachedValue$PGk05LGb$cge4dj0;
public Object call() throws Exception {
return MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptAbstract(this, this.mockitoInterceptor, null, cachedValue$PGk05LGb$cge4dj0, new Object[0]);
}
@Override
public MockMethodInterceptor getMockitoInterceptor() {
return this.mockitoInterceptor;
}
@Override
public void setMockitoInterceptor(MockMethodInterceptor mockMethodInterceptor) {
this.mockitoInterceptor = mockMethodInterceptor;
}
static {
cachedValue$PGk05LGb$cge4dj0 = Callable.class.getMethod("call", new Class[0]);
}
}
说明:如上,代理类的逻辑比较简单。Byte Buddy 生成的代理 Callable$MockitoMock$1568092145 的核心逻辑都委托给了 MockMethodInterceptor 处理。Mockito-1.x 版本中是采用 cglib 动态代理,2.x 版本改用了 bytebuddy。关于代理类的逻辑就说这么多,继续向下分析。
public class MockMethodInterceptor implements Serializable {
final MockHandler handler; // (核心类)
private final MockCreationSettings mockCreationSettings;
private final ByteBuddyCrossClassLoaderSerializationSupport serializationSupport;
public MockMethodInterceptor(MockHandler handler, MockCreationSettings mockCreationSettings) {
this.handler = handler;
this.mockCreationSettings = mockCreationSettings;
serializationSupport = new ByteBuddyCrossClassLoaderSerializationSupport();
}
Object doIntercept(Object mock, Method invokedMethod, Object[] arguments,
RealMethod realMethod) throws Throwable {
return doIntercept(mock, invokedMethod, arguments, realMethod, new LocationImpl());
}
Object doIntercept(Object mock, Method invokedMethod, Object[] arguments,
RealMethod realMethod, Location location) throws Throwable {
return handler.handle(createInvocation(mock, invokedMethod, arguments, realMethod, mockCreationSettings, location));
}
public static class DispatcherDefaultingToRealMethod {
@RuntimeType
public static Object interceptAbstract(@This Object mock,
@FieldValue("mockitoInterceptor") MockMethodInterceptor interceptor,
@StubValue Object stubValue, @Origin Method invokedMethod,
@AllArguments Object[] arguments) throws Throwable {
if (interceptor == null) {
return stubValue;
}
return interceptor.doIntercept(mock, invokedMethod, arguments, RealMethod.IsIllegal.INSTANCE);
}
}
}
说明:MockMethodInterceptor 拦截器最主要的功能包括组装执行参数和执行mock方法两个功能。核心的逻辑都委托给了 MockHandler。
- 组装执行参数。DefaultInvocationFactory.createInvocation 创建执行参数 Invocation,它包含了执行对象(mock),执行的方法(invokedMethod) ,参数(arguments)。
- 执行mock方法。最终 mock 方法执行都委托给了 MockHandler.handle(Invocation)。
1.2 执行阶段
这部分的核心是 MockHandler。
1.2.1 整体流程
通过 Mockito.mock(Class) 创建的代理类后,首先需要注册方法对应的 stub,这些 stub 封装了该方法 Invocation 及其对应的行为 Answer,执行时则根据具体的方法来匹配这些 stub。
- 注册过程:当调用
when(mock.call()).thenReturn("first")
时,会在 MockHandler 中添加该方法对应的 stub。 - 调用过程:首先根据 Invocation 中的 “方法 invokedMethod” 和 “参数 arguments” 匹配对应的 stub。如果匹配上,则返回对应的 stub 执行,如果没有则返回默认值。
说明:MockHandler 是 Mockito 的核心处理模块,它持有 InvocationContainer 容器,可以根据方法 Invocation 注册和查找对应的 stub。当 Mockito.mock(Class) 生成代理类时,通过 MockHandlerFactory 工厂类创建 MockHandler,并设置到代理类中。
public class MockHandlerFactory {
public static <T> MockHandler<T> createMockHandler(MockCreationSettings<T> settings) {
MockHandler<T> handler = new MockHandlerImpl<T>(settings);
MockHandler<T> nullResultGuardian = new NullResultGuardian<T>(handler);
return new InvocationNotifierHandler<T>(nullResultGuardian, settings);
}
}
说明:这几个 MockHandler 功能如下,重点是 MockHandlerImpl。
- NullResultGuardian:用于返回默认值,具体见 Primitives.defaultValue。
- InvocationNotifierHandler:看类名就知道是在 mock 方法执行完成后触发事件监听。
- MockHandlerImpl:(核心类)承担最核心的功能逻辑。MockHandlerImpl 中相关的核心类如下:
- InvocationContainerImpl:其 stubbed 集合变量中存储所有自定义的 mock 方法。初始化阶段将 mock 方法包装成 StubbedInvocationMatcher 添加到集合中,执行时通过 Invocation 查找。
- StubbedInvocationMatcher:具体的 stub,包含 Answer 和 Invocation 信息。它实现了 InvocationMatcher 和 Answer 接口,InvocationMatcher.matches(Invocation) 查看方法是否匹配,Answer.answer(InvocationOnMock) 则返回 mock 的执行结果。
- Invocation:封装了执行的对象(mock),方法(invokedMethod) ,参数(arguments)。
1.2.2 调用过程
当调用代理类 mock.call() 方法时,最终会回调 MockHandlerImpl.handle(Invocation) 处理。该方法主要主要做了两件事,一是根据方法参数 invocation 查找 stub;二是执行 stub,如果没有则返回默认值。
- 查找 stub。调用 invocationContainer.findAnswerFor(invocation),根据方法的参数信息 invocation 从 invocationContainer 中获取注册的 stub。
- 执行 stub。如果已经注册了 stub,则直接调用 stubbing.answer(invocation) 返回结果。否则通过 mockSettings.getDefaultAnswer().answer(invocation) 返回默认值。
设置上下文环境。调用 invocationContainer.setInvocationForPotentialStubbing(invocationMatcher),将当前正在执行的方法参数信息 invocationMatcher,设置到 invocationContainer,这样 invocationContainer 就可以获取当前正在执行的方法信息 invocation。
public class MockHandlerImpl<T> implements MockHandler<T> {
InvocationContainerImpl invocationContainer;
MatchersBinder matchersBinder = new MatchersBinder();
private final MockCreationSettings<T> mockSettings;
public MockHandlerImpl(MockCreationSettings<T> mockSettings) {
this.mockSettings = mockSettings;
this.matchersBinder = new MatchersBinder();
this.invocationContainer = new InvocationContainerImpl(mockSettings);
}
public Object handle(Invocation invocation) throws Throwable {
// 1. invocationMatcher用于匹配方法对应的stubbing
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(), // 通常返回 null
invocation // 参数信息
);
// 2. 设置当前正在执行的方法参数
invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
// 3. 根据invocation方法和参数,匹配stubbing。匹配成功则返回stub,否则返回默认值
StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
if (stubbing != null) {
// 3.1 已经注册的stub
stubbing.captureArgumentsFrom(invocation);
return stubbing.answer(invocation);
} else {
// 3.2 返回默认值
Object ret = mockSettings.getDefaultAnswer().answer(invocation);
DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
return ret;
}
}
}
1.2.3 注册过程
下面我们看
Mockito.``_when_``(``mock.call()).thenReturn(``**"**first**"**``)
发生了什么事?当我们 mock 一个对象的方法时,需要提供根据方法信息查找对应的 mock 方法的功能。
- 首先,当调用
mock.call()
时,会设置 invocationContainer 的上下文环境 invocationMatcher,也就是方法信息 invocationForStubbing。 - 其次,当调用
thenReturn(``**"**first**"**``)
时,将返回值包装成 answer。最后,将 “方法参数信息 invocation” 和 “执行逻辑 answer” 包封装到 StubbedInvocationMatcher,并添加到 stubbed 集合中。 - 总之,StubbedInvocationMatcher 有了 “方法参数信息 invocationForStubbing” 和 “执行逻辑 answer”,执行时就可以通过 invocationForStubbing 匹配对应的 stub,并调用 answer 返回执行结果。
最后,我们来看一下 InvocationContainerImpl 代码,无非就是一个容器,提供了注册和查找 stub 的功能。
// stubbed 集合保存所有注册的stub
private final LinkedList<StubbedInvocationMatcher> stubbed = new LinkedList<StubbedInvocationMatcher>();
// 当前正在执行方法的上下文-方法参数信息
private MatchableInvocation invocationForStubbing;
public StubbedInvocationMatcher addAnswer(Answer answer, boolean isConsecutive, Strictness stubbingStrictness) {
Invocation invocation = invocationForStubbing.getInvocation();
synchronized (stubbed) {
Strictness effectiveStrictness = stubbingStrictness != null ? stubbingStrictness : this.mockStrictness;
// (核心)invocationForStubbing是方法执行参数,而answer则是mock的执行逻辑
stubbed.addFirst(new StubbedInvocationMatcher(answer, invocationForStubbing, effectiveStrictness));
return stubbed.getFirst();
}
}
public StubbedInvocationMatcher findAnswerFor(Invocation invocation) {
synchronized (stubbed) {
for (StubbedInvocationMatcher s : stubbed) {
if (s.matches(invocation)) {
s.markStubUsed(invocation);
invocation.markStubbed(new StubInfoImpl(s));
return s;
}
}
}
return null;
}
2. 注解驱动
总结时刻
推荐阅读
- 《Mockito 官网》和《Mockito Github》。
- 《Unit tests with Mockito - Tutorial》:简单的使用手册。
- 《Mockito 也能 Mock final 类和 final 方法》:Mockito-2.x 之后使用 Byte-Buddy 进行动态代理。Byte-Buddy 整合 Java Instrumentation 工具类,支持修改已加载的类。Mockito 通过修改已经加载的类,来支持 final 或 static 方法的 Mock,具体详见 InlineBytecodeGenerator#mockClass。
- 《Mockito的使用及原理浅析》:Mockito-1.x 使用 CGLIG 进行动态代理。
每天用心记录一点点。内容也许不重要,但习惯很重要!