概要
Mockito是一个强大的mock工具,本文将重点讲述Mockito的基本使用及注意事项,以及其实现mock的原理。
使用
应用场景
- 开发完成之后,我们都需要经过测试才敢把代码发布到线上。一个很普遍的问题是,我们要测试的类可能会有很多对上游依赖,这些依赖包括类、对象、资源、数据等等,正常来说如果我们缺少这些依赖的话,我们将无法完成测试。这时候,我们一般的处理方法是mock,mock本地方法的返回,mock上游接口的返回等等,使这些方法返回假定的数据,好让我们流程可以继续走下去,完成测试。
- Mock对于TDD来说也是很重要的一个功能。
- 关于Mock在单元测试中的作用,可以看看Martin Fowler对它的讨论
简单例子
事例代码基于Mockito2.18.3版本。
通常来说Mockito分为两大功能点,一是verify验证,一是stub打桩(我不知道怎么翻译好,跟立flag差不多意思)
先看看一个入门的例子
1 |
|
Mockito的使用很简单吧!相信经过上述代码的演示还有注释的说明,相信大家已经对Mockito有一个大概的认识。
当然,上述代码只是演示了Mockito的一小部分功能,它还有更多、更强大的功能,我这里不将一一介绍。更多API请参考官方文档
注解使用
在我们的工程中,90%的情况都是基于Spring,使用JUnit作为单元测试框架,我们看看Mockito怎么与它们结合一起使用。
看一个完整的JUnit单元测试类的例子(假设InjectMockService依赖MockServiceA和SpyServiceB)
1 | (SpringJUnit4ClassRunner.class) |
上述代码是我瞎编的,没有跑过。不过一般的使用场景也应该长的类似。
对Mockito的注解说明一下:
- @Mock,你需要mock的对象,其原理跟上述提到的
Mockito.mock(List.class);
差不多,即把注解标记的这个对象转换成Mockito的对象 - @Spy,你需要进行部分mock的对象。这个Spy(翻译就是间谍啦)跟Mock相比,Mock是对对象的所有方法都进行mock处理的,即方法不会真正的执行,举之前的那个例子,
mockedList.add("one");
执行后不会把“one”加入到List中,但如果mockedList
是一个Spy的话就可以,即把Mockito.mock(List.class);
改为Mockito.spy(ArrayList.class);
,那么add方法就会真正的执行,这里把List改为ArrayList是因为如果遇到abstract方法也不会执行的。Spy是有使用场景的,当我们需要mock一部分方法,而另外的方法需要正常执行时就需要用到Spy了,不然你就要mock全部用到的方法了 - @InjectMocks,你需要注入@Mock对象的对象,即@InjectMocks这个对象依赖其他mock对象。这个有点像依赖注入,它就是用来解决这个问题的。举个例子说明,ServiceB依赖ServiceA,你是需要测试ServiceB中的某个方法,但是这个方法依赖到ServiceA了,ServiceA的返回你不可控,你需要mock它,这时就要把@Mock作用于ServiceA,@InjectMocks作用于ServiceB。
注意踩坑
这是我在使用Mockito遇到的一些坑,希望大家在实践中也注意一下。
@InjectMocks
@InjectMocks的作用是把@Mock的对象注入到属性中去,我们如果只写成(假设InjectMockService依赖MockServiceA)
1 |
|
这样的话,虽然InjectMockService会做初始化,而且MockServiceA会被注入到InjectMockService里,但是InjectMockService中其他的依赖对象会为null,因为你都没做其他初始化动作嘛。这时你就需要跟Spring的注入一起用,加上@Autowired注解,如下所示
1 |
|
从上Mockito的初始化得知,即上述代码的mockInit()
方法,Mockito的初始化是在Spring的后面的,所以InjectMockService会被Spring初始化,然后再被Mockito修改依赖的Mock对象。
深层次对象
实际应用时,如果你想mock的对象在比较深的调用层次,那么做法可以是:
获取依赖这个对象的对象(通过Spring的@Autowired),即mock对象的上一层对象,然后使用一些手段把mock对象替换调。
例如使用ReflectionTestUtils.setField(upperObject, "fieldName", mockObject);
把mock设置进去替换掉原来的对象。
Spy对象执行报错
在上面的例子介绍的用法中Mockito.when(mockedList.get(1)).thenReturn("13");
但是如果mockedList是一个Spy对象的话,可能会有问题。
这是因为@Spy对象会真正的实际执行该方法(@Mock则不会),但这是你要mock的方法,那么就有可能有问题。所以如果你不想方法实际执行,需要改变一下用法:
//不会调用stub的方法Mockito.doReturn(false).when(spyJack).go();
与Spring AOP
如果你Mock的对象被Spring AOP进行过处理,例如加了@Transactional
或者自己做了切面等等,这时Spring会生成代理对象,这时要注意对你Mock对象有没有产生影响。
实现原理
源码基于Mockito2.18.3版本。
上面介绍了Mockito的基本用法及注意事项。
通过API的使用,大家一定很好奇Mockito究竟是怎么实现的。
我们先尝试通过表象来推测其实现原理,然后再分析代码证实猜想。
表象猜测
如果你有多做测试,细心观察,就会发现通过Mockito生成的mock对象是一个“假”对象。
对于文章开头的例子,我们可以发现
mockedList.add("one");
不会实际往list增加一个“one”的元素(Spy则会),所以add之后你再get也是得不到结果的。即你无论怎么操作list都是徒劳的。Mockito.verify(mockedList).add("one");
这句很明显就是会校验list之前执行过的方法及入参。Mockito.when(mockedList.get(1)).thenReturn("13");
这句看上有点奇怪,怎么会用mockedList.get(1)
作为入参呢?其实想想就知道不可能的,所以这里应该也是跟上面一样,只是get方法做了记录,然后再return值。
生成Mock对象
通过上面的猜测,我们很容易猜到Mockito用了代理生成了mock对象。
我们直接跟踪Mockito.mock
方法,到MockitoCore
的mock方法
1 | public <T> T mock(Class<T> typeToMock, MockSettings settings) { |
MockUtil
的createMock方法
1 | public static <T> T createMock(MockCreationSettings<T> settings) { |
SubclassByteBuddyMockMaker
1 |
|
SubclassBytecodeGenerator
1 |
|
生成代理类的关键代码。Mockito使用的是ByteBuddy这个框架,它并不需要编译器的帮助,而是直接生成class,然后使用ClassLoader来进行加载,感兴趣的可以深入研究,其地址为:https://github.com/raphw/byte-buddy。如果你有使用过其他字节码生成框架,如Cglib或Javassist,就可以大概猜到这里做了什么事情。
这里很重要的一点就是把MockMethodInterceptor
作为代理类的拦截器。
最后就是生成字节码然后动态加载到JVM中。
Mockito生成的代理类
那么Mockito利用ByteBuddy生成的代理类是长怎么样子的,才能知道它是怎么做拦截的。就好像你看到JDK Proxy生成的代理类,就会更清楚InvocationHandler的实现一样。
还是文章开头的例子
我们可以看到几点关键的
- 持有一个MockMethodInterceptor对象
- 继承了MockAccess
- List原来的方法都被DispatcherDefaultingToRealMethod拦截了
代理拦截interceptor
好,我们现在有目标了。DispatcherDefaultingToRealMethod
是MockMethodInterceptor
的内部类,最终它还是调用MockMethodInterceptor的doIntercept
1 | Object doIntercept(Object mock, |
verify
说了mock对象是如何生成的,就可以开始说用法的实现了。
上面我们猜测verify是对调用做了记录,下面我们来证实一下。
verify start
Mockito
的verify
1 |
|
VerificationMode
有很多中类型,代表了verify的种类。
从名字中大概可以猜到有什么用,大家可以去尝试一下。
继续跟踪verify到MockitoCore
的verify
1 | public <T> T verify(T mock, VerificationMode mode) { |
MockingProgressImpl
的verificationStarted
1 | public void verificationStarted(VerificationMode verify) { |
这里注意,MockingProgressImpl是ThreadLocal的,而且它的verificationMode只有一个,就说明verify之后紧接着的mock调用就是针对这次verify的,如果多次verify的话,后者会覆盖前者。
verify start就这么多了,最重要的就是记录VerificationMode
。
verify match
记录了VerificationMode,那么如果下次调用怎么去匹配是刚刚的verify的呢?
上面说了所有mock对象的方法调用都会被MockMethodInterceptor
拦截,而MockMethodInterceptor最终会调到MockHandlerImpl
的handle方法,一直憋了很久的MockHandlerImpl终于要出场了。
这里除了verify,还有stub的,因为这个方法是包含了mock相关的所有核心逻辑了。
1 | //Invocation即InterceptedInvocation,把代理类拦截时的方法调用即参数封装在一起 |
好,我们看看Times这个VerificationMode的verify方法
1 |
|
MissingInvocationChecker
1 | public static void checkMissingInvocation(List<Invocation> invocations, MatchableInvocation wanted) { |
通过调试发现在InvocationMatcher
判断两个Invocation是否匹配
1 |
|
verify的过程就是这样了,如果找到匹配的Invocation则正常返回,找不到Mockito是会抛出异常提示信息,像assert那样子,大家可以尝试一下。
stub
stub就是可以指定返回,一般我们用的更多,从上面verify的实现过程,我们应该也能大概猜到stub也有点类似。
when
跟踪when方法直到MockitoCore
的when
1 | public <T> OngoingStubbing<T> when(T methodCall) { |
when方法就是返回上次mock方法调用封装好的OngoingStubbing。
thenReturn
thenReturn是BaseStubbing的方法,其实它也是一个OngoingStubbing
OngoingStubbing的继承关系
1 |
|
那么下次的调用怎么匹配到answer呢?
还是在MockHandlerImpl
的handle,这里只截图相关代码
1 | if (stubbing != null) { |
先看看Answer的继承关系,看看Mockito支持什么样的对stub的返回
我们用回刚刚的例子,对应的Answer就是Returns
1 | public Object answer(InvocationOnMock invocation) throws Throwable { |
优点
Mockito最大的优点是其简单,实用,优雅的API,这是作者的初衷。
Java的Mock工具还有JMock,easymock等等,大家可以对比一下。
总结
本文介绍了Mockito的基本使用及注意事项,相信看了之后你已经可以在日常工作中用上。还源码分析了其实现mock的原理,通过源码看到,表面看上去挺神奇的一个东西,其实现也不是很难的,主要用到代理+状态来控制mock对象。
参考资料
https://infoq.cn/article/mockito-design
https://blog.saymagic.cn/2016/09/17/understand-mockito.html