本文包含以下内容:
- 如何使用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()函数,基本类似。