JAVA

Mockito的使用及原理及分析

本文包含以下内容:

  • 如何使用Mockito写单元测试
  • Mockito实现原理浅析
  • 模仿Mockito实现mock功能

前言


上一篇讲述了如何编写健壮的单元测试,其中解决外部数据依赖的方式就是Mock数据返回,那么具体如何Mock数据呢?其实现机制又是怎样的呢?

Mock最常用的框架之一是Mockito,以下分析都将基于Mockito展开。

Mockito


一次完整的Mock,包括

  • 设定目标设置
  • 消费条件
  • 预期返回结果
  • 消费并检验返回结果

我们首先看一个最简单的的例子来看下如何使用Mockito来进行Mock数据返回:

 when(productService.getProductInfo(any())).thenAnswer(invocationOnMock -> {
                List<ProductInfo> productInfos = new ArrayList<>();
                return productInfos;
            });
    
    //...        
    List<ProductInfo> productInfos  = productService.getProductInfo(1);

在上述例子中,这些条件是如何对应的呢?

  • 设定目标 > List<ProductInfo> productInfos
  • 设置消费条件 -> productService.getProductInfo(any())
  • 预期返回结果 -> thenAnswer(…)
  • 消费并检验返回结果 -> productService.getProductInfo(1)

所以不难发现,Mockito主要就是通过Stub打桩,通过方法名加参数来准确的定位测试桩然后返回预期的值

提出疑问


我们都知道,Java的程序调用是用堆栈来实现的,那么是不是有这样的疑问:

when()消费的应该是productService.getProductInfo()函数的返回值,对其内部实现并不感知的,那么它是如何来准确通过函数名加参数的条件来打桩的呢?

Mockito的实现原理


首先,我们应该知道,Mock本质上是一个Proxy代理模式的应用。

Proxy模式,是在对象提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。

所以Mockito本质上就是在代理对象调用方法前,用stub的方式设置其返回值,然后在真实调用时,用代理对象返回起预设的返回值。

验证


查看when()源码

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

发现所有的methodCall或被转换成OngoingStubbing对象,而OngoingStubbing存储了哪些信息呢?

public Object handle(Invocation invocation) throws Throwable {
     if (invocationContainerImpl.hasAnswersForStubbing()) {
         ...
    }
     ...
     InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
            mockingProgress.getArgumentMatcherStorage(),
           invocation
    );
   mockingProgress.validateState();
    // if verificationMode is not null then someone is doing verify()
   if (verificationMode != null) {
   ...
      }
    // prepare invocation for stubbing   invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
  OngoingStubbingImpl<T> ongoingStubbing = 
  new OngoingStubbingImpl<T>(invocationContainerImpl);
mockingProgress.reportOngoingStubbing(ongoingStubbing);
   ...
}

查看上面这段代码,可以发现方法调用的信息(invocation)对象被用来构造invocationMatcher对象,最终传递给了ongoingStubbing对象。完成了stub信息的保存。

所以Mockito在构造时,不仅仅保存了方法的返回值,还做了大量处理,保存了stub的调用信息,才能准确定位。

而查看thenAnswer的代码,发现了这样的Demo:

public Integer answer(InvocationOnMock invocation) throws Throwable {
        return (Integer) invocation.getArguments()[0];
    }

那么我们就应该可以在answer中拿到invocation的信息啊,于是我尝试了一下:

@Test
    public void test2() {
        LinkedList mock = mock(LinkedList.class);
        when(mock.add(anyString())).thenAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                Object mock = invocation.getMock();
                return "called with arguments: " + args;
            }
        });

        System.out.println(mock.add("foo"));
    }

发现确实可以在方法中拿到调用函数以及参数信息。

所以在Mockiton中,在设置条件时,Mockito并不是对内部实现不感知,相反,保存了参数名以及入参信息,最终来构建stub,返回信息。

自己动手写Mockito Demo


学习了Mockito实现原理之后,发现其实它本质上就是通过代理 + 反模式打桩实现的。那么可以自己实现一个Mockito么?

参考相关资料后,发现应该是可行的,并找到类似材料,那么试试吧。

实现 mock


Mock的实现关键是,实现动态代理,被 mock 的对象只是“假装”调用了该方法,然后返回假的值。

可以使用cglib来进行动态代理。通过class对象创建该对象的动态代理对象,然后设置该对象的父类与回调即可。并在回调函数中定义拦截器,实现自定义逻辑。

public class Mockito {

    /**
     * 根据class对象创建该对象的代理对象 
     * 1、设置父类;2、设置回调 
     * 本质:动态创建了一个class对象的子类 
     *
     * @return
     */
    public static <T> T mock(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MockInterceptor());
        return (T)enhancer.create();
    }

    private static class MockInterceptor implements MethodInterceptor {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            return null;
        }
    }
}

实现 stub


首先定义一个类,来表示对函数的调用,重写equals()方法,通过函数名 + 参数列表来判断调用是否相同。

public class Invocation {
    private final Object mock;
    private final Method method;
    private final Object[] arguments;
    private final MethodProxy proxy;

    public Invocation(Object mock, Method method, Object[] args, MethodProxy proxy) {
        this.mock = mock;
        this.method = method;
        this.arguments = copyArgs(args);
        this.proxy = proxy;
    }

    private Object[] copyArgs(Object[] args) {
        Object[] newArgs = new Object[args.length];
        System.arraycopy(args, 0, newArgs, 0, args.length);
        return newArgs;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !obj.getClass().equals(this.getClass())) { return false; }
        Invocation other = (Invocation)obj;
        return this.method.equals(other.method) && this.proxy.equals((other).proxy)
            && Arrays.deepEquals(arguments, other.arguments);
    }

    @Override
    public int hashCode() {
        return 1;
    }
}

接下来,在 MockInterceptor 类中,需要做两个操作。

  • 为了设置方法的返回值,需要存放对方法的引用(Invocation)
  • 调用方法时,检查是否已经设置了该方法的返回值(results)。如果设置了,则返回该值。
public class Mockito {

    private static Map<Invocation, Object> results = new HashMap<Invocation, Object>();
    private static Invocation lastInvocation;

    public static <T> T mock(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MockInterceptor());
        return (T)enhancer.create();
    }

    private static class MockInterceptor implements MethodInterceptor {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            Invocation invocation = new Invocation(proxy, method, args, proxy);
            lastInvocation = invocation;
            if (results.containsKey(invocation)) {
                return results.get(invocation);
            }
            return null;
        }
    }

    public static <T> When<T> when(T o) {
        return new When<T>();
    }

    public static class When<T> {
        public void thenReturn(T retObj) {
            results.put(lastInvocation, retObj);
        }
    }
}

测试


测试用例如下:

@Test
    public void test() {
        Calculate calculate = mock(Calculate.class);
        when(calculate.add(1, 1)).thenReturn(1);
        Assert.assertEquals(1, calculate.add(1, 1));
    }

其他


Mockito打桩返回的方式有很多,这边主要关注了经常使用的thenAnswer()函数,至于其他thenReturn()、thenThrow()、thenCallRealMethod()、then()函数,基本类似。