常用的 Mock Toolkit 有 EasyMock、JMockit、Mockito、PowerMock 等,而本文主要介绍 Mockito 的简单使用,并对其实现原理进行分析。
1. 工作原理
Mockito 的使用方法如下,具体使用方法参考 Unit tests with Mockito - Tutorial[1]:
@Testpublic 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;@FunctionalInterfacepublic 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]);}@Overridepublic MockMethodInterceptor getMockitoInterceptor() {return this.mockitoInterceptor;}@Overridepublic 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 {@RuntimeTypepublic 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用于匹配方法对应的stubbingInvocationMatcher invocationMatcher = matchersBinder.bindMatchers(mockingProgress().getArgumentMatcherStorage(), // 通常返回 nullinvocation // 参数信息);// 2. 设置当前正在执行的方法参数invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);// 3. 根据invocation方法和参数,匹配stubbing。匹配成功则返回stub,否则返回默认值StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);if (stubbing != null) {// 3.1 已经注册的stubstubbing.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 集合保存所有注册的stubprivate 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 进行动态代理。
每天用心记录一点点。内容也许不重要,但习惯很重要!
