本文包含以下内容:
- 如何使用Mockito写单元测试
- Mockito实现原理浅析
- 模仿Mockito实现mock功能
前言
上一篇讲述了如何编写健壮的单元测试,其中解决外部数据依赖的方式就是Mock数据返回,那么具体如何Mock数据呢?其实现机制又是怎样的呢?
Mock最常用的框架之一是Mockito,以下分析都将基于Mockito展开。
Mockito
一次完整的Mock,包括
- 设定目标
- 设置消费条件
- 预期返回结果
- 消费并检验返回结果
我们首先看一个最简单的的例子来看下如何使用Mockito来进行Mock数据返回:
1 | when(productService.getProductInfo(any())).thenAnswer(invocationOnMock -> { |
在上述例子中,这些条件是如何对应的呢?
- 设定目标 >
List<ProductInfo> productInfos
- 设置消费条件 ->
productService.getProductInfo(any())
- 预期返回结果 ->
thenAnswer(...)
- 消费并检验返回结果 ->
productService.getProductInfo(1)
所以不难发现,Mockito主要就是通过Stub打桩,通过方法名加参数来准确的定位测试桩然后返回预期的值
提出疑问
我们都知道,Java的程序调用是用堆栈来实现的,那么是不是有这样的疑问:
when()
消费的应该是productService.getProductInfo()
函数的返回值,对其内部实现并不感知的,那么它是如何来准确通过函数名加参数的条件来打桩的呢?
Mockito的实现原理
通过查看代码,我们不难发现mock
的入口是在MockitoCore.java
中的这段代码:
1 | public <T> T mock(Class<T> typeToMock, MockSettings settings) { |
在这里mock函数接收两个参数typeToMock(mock类型)和settings(设置内容),目标是创建一个mock对象。跟随第六行createMock()
进入MockUtil.java
中,可以看见动态调用了org.mockito.internal.creation.cglib
类:
1 | public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { |
上述代码中根据MockHandler创建InternalMockHandler后,通过cglib代理来执行mock的流程,而在imposterise
实现中调用了createProxyClass()
的方法,而createProxyClass()
方法中的是实现是创建Mock对象的关键所在了:
1 | public <T> T imposterise(final MethodInterceptor interceptor, Class<T> mockedType, Class<?>... ancillaryTypes) { |
点进去createProxyClass()
查看具体的代码实现块,可以发现这里进行了很多的enhancer
设置。Enhancer允许为非接口类型创建一个Java代理。Enhancer动态创建了给定类型的子类但是拦截了所有的方法。
1 | public Class<Factory> createProxyClass(Class<?> mockedType, Class<?>... interfaces) { |
而通过enhancer是如何创建Mock类对象的呢?
1 | if (gen == null) { |
在上述代码中,使用strategy.generate(this)
生成字节码的字节数组,再通过ReflectUtils.defineClass(className, b, loader)
将字节数组转换成class对象,以此来创建Mock类。
所以,Mock本质上是一个Proxy代理模式的应用。
Proxy模式,是在对象提供一个proxy对象,所有对真实对象的调用,都先经过proxy对象,然后由proxy对象根据情况,决定相应的处理,它可以直接做一个自己的处理,也可以再调用真实对象对应的方法。
所以Mockito本质上就是在代理对象调用方法前,用stub的方式设置其返回值,然后在真实调用时,用代理对象返回起预设的返回值。
验证
查看when()
源码
1 | public <T> OngoingStubbing<T> when(T methodCall) { |
发现所有的methodCall或被转换成OngoingStubbing对象,而OngoingStubbing存储了哪些信息呢?
1 | public Object handle(Invocation invocation) throws Throwable { |
查看上面这段代码,可以发现方法调用的信息(invocation)对象被用来构造invocationMatcher对象,最终传递给了ongoingStubbing对象。完成了stub信息的保存。
所以Mockito在构造时,不仅仅保存了方法的返回值,还做了大量处理,保存了stub的调用信息,才能准确定位。
而查看thenAnswer的代码,发现了这样的Demo:
1 | public Integer answer(InvocationOnMock invocation) throws Throwable { |
那么我们就应该可以在answer中拿到invocation的信息啊,于是我尝试了一下:
1 |
|
发现确实可以在方法中拿到调用函数以及参数信息。
所以在Mockiton中,在设置条件时,Mockito并不是对内部实现不感知,相反,保存了参数名以及入参信息,最终来构建stub,返回信息。
自己动手写Mockito Demo
在学习了Mockito实现原理之后,发现其实它本质上就是通过代理 + 反模式打桩实现的。那么可以自己实现一个Mockito么?
参考相关资料后,发现应该是可行的,并找到类似材料,那么试试吧。
实现 mock
Mock的实现关键是,实现动态代理,被 mock 的对象只是“假装”调用了该方法,然后返回假的值。
可以使用cglib来进行动态代理。通过class对象创建该对象的动态代理对象,然后设置该对象的父类与回调即可。并在回调函数中定义拦截器,实现自定义逻辑。
1 | public class Mockito { |
实现 stub
首先定义一个类,来表示对函数的调用,重写equals()方法,通过函数名 + 参数列表来判断调用是否相同。
1 | public class Invocation { |
接下来,在 MockInterceptor 类中,需要做两个操作。
- 为了设置方法的返回值,需要存放对方法的引用(Invocation)
- 调用方法时,检查是否已经设置了该方法的返回值(results)。如果设置了,则返回该值。
1 | public class Mockito { |
测试
测试用例如下:
1 |
|
其他
Mockito打桩返回的方式有很多,这边主要关注了经常使用的thenAnswer()
函数,至于其他thenReturn()
、thenThrow()
、thenCallRealMethod()
、then()
函数,基本类似。