Mockito原理解析

Posted by Eric on Saturday, July 15, 2023

前言

Mockito是一款Java主流的单元测试框架,上手简单,mockito官网:http://mockito.org,API文档:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html,项目源码:https://github.com/mockito/mockito

在我们写单元测试时,经常遇到待测试类依赖于多个类,从而形成一个大的依赖树,要在单元测试中完整的构建这一依赖是一件非常困难的事情。Mockito通过mock方法和stub方法,可以构建一个mock对象,然后对mock对象的行为进行打桩,让我们把单元测试聚焦于待测试类上。

然而,Mockito是如何实现这一功能的呢?本文就when方法和stub方法对其进行介绍。

单测环境

Maven包的引入:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.0.0</version>
</dependency>

被测试类A:

public class A {

    public String aMethod(){
        return "hello world!";
    }
}

单侧类:

public class ATest {

    @Test
    public void testA(){
        A mock = Mockito.mock(A.class);
        Mockito.when(mock.aMethod()).thenReturn("ABC");
    }

}

mock对象的生成

mock对象的基本原理是生成一个被mock对象的代理类,然后对该类的方法进行拦截、代理增强。

mock对象的核心代码如下所示:

// org.mockito.internal.util.MockUtil
public static <T> T createMock(MockCreationSettings<T> settings) {
    MockHandler mockHandler =  createMockHandler(settings);
    T mock = mockMaker.createMock(settings, mockHandler);
    Object spiedInstance = settings.getSpiedInstance();
    if (spiedInstance != null) {
        new LenientCopyTool().copyToMock(spiedInstance, mock);
    }
    return mock;
}

总结起来流程如下:

image-20230715221937978

Mockito中mock对象的生成使用ByteBuddy技术。

我们以Object类为例,通过重写它的toString方法,用来返回“Hello World!”来讲解Byte Buddy的使用方法。

Class<?> dynamicType = new ByteBuddy()
        .subclass(Object.class)
        .method(ElementMatchers.named("toString"))
        .intercept(FixedValue.value("Hello World!"))
        .make()
        .load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
        .getLoaded();

比如我们想看下上述示例中生成的动态类到底长什么样子,可以调用方法 net.bytebuddy.dynamic.DynamicType#saveIn 来将生成的class保存成class文件(可在debug时再watcher中调用saveIn方法),然后在IDEA中进行查看,如下所示:

public class Object$ByteBuddy$F6rbnunr {
    public String toString() {
        return "Hello World!";
    }

    public Object$ByteBuddy$F6rbnunr() {
    }
}

在Mockito中,动态生成代理类class对应的方法如下

// org.mockito.internal.creation.bytebuddy.SubclassBytecodeGenerator
public <T> Class<? extends T> mockClass(MockFeatures<T> features) {
        // ...
				// Byte Buddy动态生成类
        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示例代码原理一样,其最终会生成A的代理类如下

public class A$MockitoMock$883113934 extends A 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$9GiAqxT8$4cscpe1, new Object[0], new A$MockitoMock$883113934$auxiliary$kDQsaeNa(this));
    }

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

    protected Object clone() throws CloneNotSupportedException {
        return DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$9GiAqxT8$7m9oaq0, new Object[0], new A$MockitoMock$883113934$auxiliary$RfEBiKk5(this));
    }

    public String aMethod() {
        return (String)DispatcherDefaultingToRealMethod.interceptSuperCallable(this, this.mockitoInterceptor, cachedValue$9GiAqxT8$cqor5a0, new Object[0], new A$MockitoMock$883113934$auxiliary$Av33OzJ6(this));
    }

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

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

    public A$MockitoMock$883113934() {
    }
}

可以看到aMethod方法被改写成了调用方法分发器DispatcherDefaultingToRealMethod,我们看下它的interceptSuperCallable方法

// DispatcherDefaultingToRealMethod
@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)
                                  );
}

可见其主要是调用了interceptor.doIntercept方法,interceptor是mockA中的MockMethodInterceptor对象。MockMethodInterceptor对象中持有MockHandler对象,而MockMethodInterceptor#doIntercept实际上就是调用了MockHandler#handle。简言之,被mock对象的公有方法都被MockHandler类拦截了(其实是被MockMethodInterceptor拦截,然后交由MockHandler处理)。

此外,在mock对象生成后,也会生成MockingProgress对象,该对象被保存在ThreadLocal中

// org.mockito.internal.MockCore
public <T> T mock(Class<T> typeToMock, MockSettings settings) {
    ...
    T mock = createMock(creationSettings);
    mockingProgress().mockingStarted(mock, creationSettings);
    return mock;
}

该对象用于存储在mockito代码执行过程中各种信息,将在stub过程中起到很重要的作用。

对象如何打桩stub

一种最常见的打桩语句如下所示

// mock为A类的mock对象
Mockito.when(mock.aMethod()).thenReturn("ABC");

以上语句可以分为三步:

  1. mock.aMethod()方法调用
  2. Mockito.when()静态方法调用
  3. .thenReturn()打桩

打桩前的方法调用

在上文中我们已经知道,mock对象的方法被MockMethodInterceptor所拦截,然后调用了MockHandler#handle方法。

MockHandler结构如下图所示

image-20230716111548358

其中

  • Invocation:用来描述一次方法的调用
  • ArgumentMatcher:用来描述方法调用中参数的匹配方法
  • Answer:用来描述打桩行为,thenReturn即一种Answer
// org.mockito.internal.handler.MockHanlderImpl#handle
public Object handle(Invocation invocation) throws Throwable {
  	// 这个分支是处理doAnswer(xx).when(mock).methodXX()的打桩方式
    // 这是一种给void返回类型方法打桩的方式
    // 1. 执行doAnswer后会返回一个Stubber对象,其中持有List<Answer>
    // 2. 执行.when方法后,会将List<Answer>拷贝到doAnswerStyleStubbing中
    if (invocationContainer.hasAnswersForStubbing()) {
        // stubbing voids with doThrow() or doAnswer() style
        InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
                mockingProgress().getArgumentMatcherStorage(),
                invocation
        );
        invocationContainer.setMethodForStubbing(invocationMatcher);
        return null;
    }
    VerificationMode verificationMode = mockingProgress().pullVerificationMode();

    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);
        }
    }

    // prepare invocation for stubbing
    // 在when中方法被第一次调用,会进入到这个逻辑
    // 将本次调用放入到invocationForStubbing中, 并添添加到registeredInvocations中
    // 生成OngoingStubbing对象,并将其存放到mockingProgess中
    invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
    OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
    mockingProgress().reportOngoingStubbing(ongoingStubbing);

    // look for existing answer for this invocation
    // 如果已经打好桩,则会在此处匹配到stub
    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);

    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;
    }
}

Mockito.when静态方法

// org.mockito.internal.MockitoCore
public <T> OngoingStubbing<T> when(T methodCall) {
    MockingProgress mockingProgress = mockingProgress();
    mockingProgress.stubbingStarted();
    @SuppressWarnings("unchecked")
    OngoingStubbing<T> stubbing = (OngoingStubbing<T>) mockingProgress.pullOngoingStubbing();
    if (stubbing == null) {
        mockingProgress.reset();
        throw missingMethodInvocation();
    }
    return stubbing;
}

可见该方法即从mockingProgress中拿到mock方法第一次调用时放入其中的OngoingStubbing对象,然后返回。

OngoingStubbing#thenReturn打桩

// org.mockito.internal.stubbing.BaseStubbing
public OngoingStubbing<T> thenReturn(T value, T... values) {
    // 会调用OngoingStubbingImpl#thenAnswer
    OngoingStubbing<T> stubbing = thenReturn(value);
    if (values == null) {
        // For no good reason we're configuring null answer here
        // This has been like that since forever, so let's keep it for compatibility (unless users complain)
        return stubbing.thenReturn(null);
    }
    for (T v : values) {
        stubbing = stubbing.thenReturn(v);
    }
    return stubbing;
}
// org.mockito.internal.stubbing.OngoingStubbingImpl
public OngoingStubbing<T> thenAnswer(Answer<?> answer) {
    // 判断收否存在待打桩的调用
    if(!invocationContainer.hasInvocationForPotentialStubbing()) {
        throw incorrectUseOfApi();
    }
		// 往stubbed中添加打桩行为
    invocationContainer.addAnswer(answer, strictness);
    return new ConsecutiveStubbing<T>(invocationContainer);
}

image-20230716110958425