聊一聊 Mockito

1,362 阅读9分钟

这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战

从一个最简单的案例看起

@Test
public void testAnswer() {
    MockitoAnswerService service = Mockito.mock(MockitoAnswerService.class);
    Mockito.when(service.getResult(Mockito.anyString(), Mockito.anyList(), Mockito.any(MockitoAnswerParam.class))).then(new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocation) throws Throwable {
            Object[] arguments = invocation.getArguments();
            // 3
            System.out.println(arguments.length);
            return "MOCK_ANSWER_RESULT";
        }
    });

    String result = service.getResult("1", new ArrayList<>(), new MockitoAnswerParam());
    Assert.assertTrue(result.equals("MOCK_ANSWER_RESULT"));
}

从代码看,主要有这样几个关键行为:

  • mock

  • when

  • then

  • answer

下面将以这几个行为作为分析的着手点。

mock 对象做了哪些事情

/**
     * Creates mock object of given class or interface.
     * <p>
     * See examples in javadoc for {@link Mockito} class
     *
     * @param classToMock class or interface to mock
     * @return mock object
     */
@CheckReturnValue
public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings());
}

接受一个 class 类型与一个 mockSettings,classToMock 就是我们需要 mock 对象的类型,而mockSettings 则记录着此次 mock 的一些信息。

mock

Mockito 内部持有了一个 MockitoCore 对象 MOCKITO_CORE, mock 最后是委托给 MOCKITO_CORE 来处理。

org.mockito.internal.MockitoCore #mock

public <T> T mock(Class<T> typeToMock, MockSettings settings) {
    if (!MockSettingsImpl.class.isInstance(settings)) {
        throw new IllegalArgumentException(
            "Unexpected implementation of '"
            + settings.getClass().getCanonicalName()
            + "'\n"
            + "At the moment, you cannot provide your own implementations of that class.");
    }
    MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
    MockCreationSettings<T> creationSettings = impl.build(typeToMock);
    // 创建 mock 对象
    T mock = createMock(creationSettings);
    mockingProgress().mockingStarted(mock, creationSettings);
    return mock;
}

mock 对象的产生

在MockitoCore中一是做了一下初始化工作,接着继续将 mock 对象创建交给了 MockUtil

org.mockito.internal.util.MockUtil#createMock

public static <T> T createMock(MockCreationSettings<T> settings) {
        MockHandler mockHandler = createMockHandler(settings);

        Object spiedInstance = settings.getSpiedInstance();

        T mock;
        if (spiedInstance != null) {
            mock =
                    mockMaker
                            .createSpy(settings, mockHandler, (T) spiedInstance)
                            .orElseGet(
                                    () -> {
                                        T instance = mockMaker.createMock(settings, mockHandler);
                                        new LenientCopyTool().copyToMock(spiedInstance, instance);
                                        return instance;
                                    });
        } else {
            mock = mockMaker.createMock(settings, mockHandler);
        }

        return mock;
    }

在 MockUtil 中比较关键的两个处理:

  • 创建 MockHandler 对象,MockHandler 是一个接口,MockHandler 对象的实例是 InvocationNotifierHandler 类型,它只是负责对外的包装,内部实际起作用的是MockHandlerImpl,这个类承载了 Mockito 的主要逻辑,后面详细说明。
  • 调用 mockMaker 来创建最终的实例,MockMaker 也是一个接口,其实现为ByteBuddyMockMaker。
public class ByteBuddyMockMaker implements ClassCreatingMockMaker {
    
    private ClassCreatingMockMaker defaultByteBuddyMockMaker = new SubclassByteBuddyMockMaker();

    @Override
    public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
        return defaultByteBuddyMockMaker.createMock(settings, handler);
    }
    // 省略其他代码
}

可以看到,ByteBuddyMockMaker 内部用于创建 mock 对象的是 SubclassByteBuddyMockMaker。具体实现如下(为方便代码阅读,这里将一些无关代码去掉了):

@Override
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
 
    Class<? extends T> mockedProxyType = createMockType(settings);

    Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings);
    T mockInstance = null;
    try {
        // 实例化
        mockInstance = instantiator.newInstance(mockedProxyType);
        MockAccess mockAccess = (MockAccess) mockInstance;
        mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings));

        return ensureMockIsAssignableToMockedType(settings, mockInstance);
    } catch (ClassCastException cce) {
        // ignore code
    } catch (org.mockito.creation.instance.InstantiationException e) {
        // ignore code
    }
}

可以比较明确的看到,其创建了一个 mock proxy(在之前的版本是直接创建代理类的),这里通过追踪代码,最终创建的逻辑在 org.mockito.internal.creation.bytebuddy.SubclassBytecodeGenerator#mockClass。这个方法内部逻辑比较多,但是核心部分就是通过 bytebuddy.net 动态生成一个代理类。

// ....
DynamicType.Builder<T> builder =
                byteBuddy
                        .subclass(features.mockedType)
                        .name(name)
                        .ignoreAlso(isGroovyMethod())
                        .annotateType(
                                features.stripAnnotations
                                        ? new Annotation[0]
                                        : features.mockedType.getAnnotations())
                        .implement(new ArrayList<Type>(features.interfaces))
                        .method(matcher)
                        .intercept(dispatcher)
                        .transform(withModifiers(SynchronizationState.PLAIN))
                        .attribute(
                                features.stripAnnotations
                                        ? MethodAttributeAppender.NoOp.INSTANCE
                                        : INCLUDING_RECEIVER)
                        .method(isHashCode())
                        .intercept(hashCode)
                        .method(isEquals())
                        .intercept(equals)
                        .serialVersionUid(42L)
                        .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE)
                        .implement(MockAccess.class)
                        .intercept(FieldAccessor.ofBeanProperty());
//....
return builder.make()
                .load(
                        classLoader,
                        loader.resolveStrategy(features.mockedType, classLoader, localMock))
                .getLoaded();

这里其实是 bytebuddy 动态生成类的基本动作,但是毕竟只是代码,下面是我将当前创建的 mockedProxyType 字节码写到 .class 文件得到的:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.testproperties;

import com.example.testproperties.MockitoAnswerService.MockitoMock.1791756687.auxiliary.UFw2zv6r;
import com.example.testproperties.MockitoAnswerService.MockitoMock.1791756687.auxiliary.oa15uI8r;
import com.example.testproperties.MockitoAnswerService.MockitoMock.1791756687.auxiliary.vbeEWzX6;
import java.util.List;
import org.mockito.internal.creation.bytebuddy.MockAccess;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.DispatcherDefaultingToRealMethod;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForEquals;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForHashCode;

public class MockitoAnswerService$MockitoMock$1791756687 extends MockitoAnswerService implements MockAccess {
    private static final long serialVersionUID = 42L;
    private MockMethodInterceptor mockitoInterceptor;

    public boolean equals(Object var1) {
        return ForEquals.doIdentityEquals(this, var1);
    }

    public String toString() {
        return (String)DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$oKHFaZjn$4cscpe1, new Object[0], new UFw2zv6r(this));
    }

    public int hashCode() {
        return ForHashCode.doIdentityHashCode(this);
    }

    protected Object clone() throws CloneNotSupportedException {
        return DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$oKHFaZjn$7m9oaq0, new Object[0], new oa15uI8r(this));
    }

    public String getResult(String arg1, List<Object> arg2, MockitoAnswerParam arg3) {
        return (String)DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$oKHFaZjn$rr3ccu3, new Object[]{var1, var2, var3}, new vbeEWzX6(this, var1, var2, var3));
    }

    public MockMethodInterceptor getMockitoInterceptor() {
        return this.mockitoInterceptor;
    }

    public void setMockitoInterceptor(MockMethodInterceptor var1) {
        this.mockitoInterceptor = var1;
    }

    public MockitoAnswerService$MockitoMock$1791756687() {
    }
}

可以比较直观的看到,getResult 方法被插了拦截器,且当前类是作为目标被测试类的子类。来看下 DispatcherDefaultingToRealMethod#interceptSuperCallable 方法:

@RuntimeType
@BindingPriority(BindingPriority.DEFAULT * 2)
public static Object interceptSuperCallable(
    @This Object mock,
    @FieldValue("mockitoInterceptor") MockMethodInterceptor interceptor,
    @Origin Method invokedMethod,
    @AllArguments Object[] arguments,
    @SuperCall(serializableProxy = true) Callable<?> superCall)
    throws Throwable {
    if (interceptor == null) {
        return superCall.call();
    }
    return interceptor.doIntercept(
        mock, invokedMethod, arguments, new RealMethod.FromCallable(superCall));
}

按照参数来看,cachedValueoKHFaZjnoKHFaZjnrr3ccu3 这坨东西居然是原始方法...。在前面那个代码中也提到,这里 interceptor 是类型为 MockMethodInterceptor 的实例,在创建字节码时就有体现这一点。所以在方法调用上,会走 doIntercept 的逻辑。

mock 方法的执行

上面提到,mock 本质上时通过 byteBuddy 创建了一个被 mock 类的子类,并且未被 mock 类方法插上了拦截器。所以我们基本就可以猜测到,mock 方法的实际执行,会被 mock 对象包装过的方法及拦截器 hook 掉,从而走到预设的逻辑。下面具体分析。(下面是通过执行堆栈追踪到的具体执行逻辑,org.mockito.internal.creation.bytebuddy.MockMethodInterceptor#doIntercept

Object doIntercept(
            Object mock,
            Method invokedMethod,
            Object[] arguments,
            RealMethod realMethod,
            Location location)
            throws Throwable {
        // If the currently dispatched method is used in a hot path, typically a tight loop and if
        // the mock is not used after the currently dispatched method, the JVM might attempt a
        // garbage collection of the mock instance even before the execution of the current
        // method is completed. Since we only reference the mock weakly from hereon after to avoid
        // leaking the instance, it might therefore be garbage collected before the
        // handler.handle(...) method completes. Since the handler method expects the mock to be
        // present while a method call onto the mock is dispatched, this can lead to the problem
        // described in GitHub #1802.
        //
        // To avoid this problem, we distract the JVM JIT by escaping the mock instance to a thread
        // local field for the duration of the handler's dispatch.
        //
        // When dropping support for Java 8, instead of this hatch we should use an explicit fence
        // https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Reference.html#reachabilityFence-java.lang.Object-
        weakReferenceHatch.set(mock);
        try {
            return handler.handle(
                    createInvocation(
                            mock,
                            invokedMethod,
                            arguments,
                            realMethod,
                            mockCreationSettings,
                            location));
        } finally {
            weakReferenceHatch.remove();
        }
    }

这里在内部,有包装了一层,将原始方法及mock 对象参数等,放到了 org.mockito.internal.invocation.InterceptedInvocation 这个类中。这种做法还是比较常见的,使得所有的模型按照组件内部封装的模型进行转换和执行,在组件内部形成语义上的闭环。weakReferenceHatch 是一个 ThreadLocal 变量,这里就是将 mock 对象放在 ThreadLocal 中,已便于后面基于当前线程上下文的所有动作都可以共享这个 mock 对象。那最核心的执行部分就是 handler#handle(Invocation invocation)。

这里的 Hander 是 MockHandler,Mockito 内部实现此接口的有三个,实际使用最多的是 MockHandlerImpl 实现方式,这里以 MockHandlerImpl 的 handle 为例。下面是代码:

public Object handle(Invocation invocation) throws Throwable {
    // doAnswer 时会执行
        if (invocationContainer.hasAnswersForStubbing()) {
            // stubbing voids with doThrow() or doAnswer() style
            InvocationMatcher invocationMatcher =
                    matchersBinder.bindMatchers(
                            mockingProgress().getArgumentMatcherStorage(), invocation);
            invocationContainer.setMethodForStubbing(invocationMatcher);
            return null;
        }
    // VerificationMode 是对验证信息的封装,它是一个接口,含有 verify 函数, 例如常用的 never,times 返回的都是 Times 类型,而 Times 类型就是 VerificationMode 的一种实现。 然后,调用 mockingProgress 来缓存 mode 信息。
        VerificationMode verificationMode = mockingProgress().pullVerificationMode();
		// 用来匹配 when 条件
        InvocationMatcher invocationMatcher =
                matchersBinder.bindMatchers(
                        mockingProgress().getArgumentMatcherStorage(), invocation);

        mockingProgress().validateState();

        // if verificationMode is not null then someone is doing verify()
        if (verificationMode != null) {
            // We need to check if verification was started on the correct mock
            // - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
            if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
                VerificationDataImpl data =
                        new VerificationDataImpl(invocationContainer, invocationMatcher);
                verificationMode.verify(data);
                return null;
            } else {
                // this means there is an invocation on a different mock. Re-adding verification
                // mode
                // - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
                mockingProgress().verificationStarted(verificationMode);
            }
        }

        // 准备 stubbing 调用
        invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
        OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
        mockingProgress().reportOngoingStubbing(ongoingStubbing);

        // 寻找此 invocation 是否有 answer
        StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
        // TODO #793 - when completed, we should be able to get rid of the casting below
        notifyStubbedAnswerLookup(
                invocation,
                stubbing,
                invocationContainer.getStubbingsAscending(),
                (CreationSettings) mockSettings);
		// 如果有 answer 则进行 answer 的回调,上面的 例子中有,所以 case 执行会走到这里
        if (stubbing != null) {
            stubbing.captureArgumentsFrom(invocation);

            try {
                return stubbing.answer(invocation);
            } finally {
                // Needed so that we correctly isolate stubbings in some scenarios
                // see MockitoStubbedCallInAnswerTest or issue #1279
                mockingProgress().reportOngoingStubbing(ongoingStubbing);
            }
        } else {
            Object ret = mockSettings.getDefaultAnswer().answer(invocation);
            DefaultAnswerValidator.validateReturnValueFor(invocation, ret);

            // Mockito uses it to redo setting invocation for potential stubbing in case of partial
            // mocks / spies.
            // Without it, the real method inside 'when' might have delegated to other self method
            // and overwrite the intended stubbed method with a different one.
            // This means we would be stubbing a wrong method.
            // Typically this would led to runtime exception that validates return type with stubbed
            // method signature.
            invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
            return ret;
        }
    }

这里说明以下,handle 方法在 when 中会被执行一次,再后面实际调用时还会执行一次,所以一次 when(method) 到实际 mock 调用,会执行两次。这里最开始在 debug 时,每次第一次走到都没符合预期返回 then 的值,后面通过分析堆栈发展调用入口时 when 中 mock 发起,并非时后面实际测试调用发起,才解释了这个没有预期返回结果的情况。

这里补充下 InvocationMatcher 的 runtime 信息

MockHandler

mockito 会为每个创建的 mock 对象创建一个 MockHandler, 它的实现类是 MockHandlerImpl。该对象主要用于处理 mock 对象的每个被拦截方法。执行每个拦截方法的时候,都会交给这个 handler 处理。

MockingProgress

用来存放正在 mock 中的数据,如 OngoingStubbingImpl、argumentMatcherStorage、verificationMode 等。MockingProgress 的实现类是 MockingProgressImpl 。从 MockHandlerImpl#handle 的代码中可以看到,在整个执行逻辑中会平凡的出现 mockingProgress() ,mockingProgress() 是 ThreadSafeMockingProgress 中提供的静态方法,ThreadSafeMockingProgress 为不用线程分配了不同的 MockingProgress ,采用了 ThreadLocal 方式实现,保证了线程安全。

OngoingStubbingImpl 

mock 方法的一个包装。会为每个 mock 对象的方法创建一个 OngoingStubbingImpl,用来监控和模拟该方法行为。如上面service.getResult(Mockito.anyString(), Mockito.anyList(), Mockito.any(MockitoAnswerParam.class)) 行为。一个 mock 方法对应多个 OngoingStubbingImpl,因为每调用一次 mock 方法都会创建一个 OngoingStubbingImpl 对象。

//see MockHandlerImpl#handle

invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
// 这里是对上述描述的代码解释
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
mockingProgress().reportOngoingStubbing(ongoingStubbing);

mockSettings

MockSettings has been introduced for two reasons. Firstly, to make it easy to add another mock setting when the demand comes. Secondly, to enable combining together different mock settings without introducing zillions of overloaded mock() methods.

引入 MockSettings 有两个原因。 首先,当有需求时,可以很容易地添加另一个模拟设置。 其次,在不引入无数重载的 mock() 方法的情况下,将不同的模拟设置组合在一起。

怎么理解这个 mockSettings ?举个例子:

创建具有不同 defaultAnswer 和 name 的 mock

Foo mock = mock(Foo.class, withSettings()
                                  .defaultAnswer(RETURNS_SMART_NULLS)
                                  .name("cool mockie")
                                  );

创建具有不同 defaultAnswer name 和 extraInterfaces 的 mock

Foo mock = mock(Foo.class, withSettings()
                                  .defaultAnswer(RETURNS_SMART_NULLS)
                                  .name("cool mockie")
                                  .extraInterfaces(Bar.class));

什么是 extraInterfaces

指定 mock 应该实现的额外接口,可能对遗留代码或某些极端情况有用。这个神秘的功能应该偶尔使用。被测对象应该确切地知道它的合作者和依赖关系。 如果实际场景碰巧有经常这样使用它的,那往往说明你的代码可能不具备简单、干净和可读的特性。

Foo foo = mock(Foo.class, withSettings().extraInterfaces(Bar.class, Baz.class));

现在,mock 实现了额外的接口,因此可以进行以下转换:

Bar bar = (Bar) foo;
Baz baz = (Baz) foo;

什么的 name

当前 mock 对象的名字,没有什么实际的用处,即使 name 相同,也是不同的两个 mock 对象(可通过 hashCode 判断得出)

Foo foo = mock(Foo.class, withSettings().name("foo"));
Foo foo = mock(Foo.class, "foo");

@Mock 注解 的 name 默认是 filedName 。

什么是 defaultAnswer

对 mock 对象的方法进行调用预期的设定,可以通过 thenReturn() 来指定返回值,thenThrow() 指定返回时所抛异常,通常来说这两个方法足以应对一般的需求。但有时我们需要自定义方法执行的返回结果,Answer 接口就是满足这样的需求而存在的,它可以用来处理那些 mock 对象没有 stubbing 的方法的返回值。

@Test
public void testAnswer() {
    MockitoAnswerService service = Mockito.mock(MockitoAnswerService.class);
    Mockito.when(service.getResult(Mockito.anyString(), Mockito.anyList(), Mockito.any(MockitoAnswerParam.class))).then(new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocation) throws Throwable {
            Object[] arguments = invocation.getArguments();
            // 3
            System.out.println(arguments.length);
            return "MOCK_ANSWER_RESULT";
        }
    });

    String result = service.getResult("1", new ArrayList<>(), new MockitoAnswerParam());
    Assert.assertTrue(result.equals("MOCK_ANSWER_RESULT"));
}

还有一些如 stubbingLookupListeners 等这里不做过多说明,详细可参考官方文档。