在软件开发的单元测试环节,外部调用和第三方代码没必要测试,因为可能无法测试,也可能会影响测试效率。为了不测试恼人的外部调用和第三方代码,我们经常需要模拟这些 *** 的返回值,这就是测试常用的mock技术。JAVA测试框架Mockito是这样的一个测试框架,本文将深入浅出Mockito的工作原理。
Mockito
Mockito使用不难,操作方便。但是问起具体的工作机制来,却没法一口气说出来,需要好好整理一番。
基本使用
使用上按照invoke-when-then-invoke这样的步骤去使用就可以了,即插桩前调用-插桩-插桩后调用。
如下案例所示,这里用的是3.9.0版本的mockito-core包:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.9.0</version> <scope>test</scope> </dependency> ***代码
A a = Mockito.mock(A.class); System.out.println("a.test() = " + a.test()); Mockito.when(a.test()).thenReturn(new MyMap()); System.out.println("a.test() = " + a.test()); ***代码返回符合预期
a.test() = null a.test() = {} ***代码插桩指的是when-then的过程,指定 *** 参数,模拟结果返回。
Mock到底做了什么?
使用看起来很简单,那么,Mock到底做了什么?怎么就修改了类的既定逻辑?
其实并没有修改原类的既定逻辑,而是调用mock工具函数会生成一个Mock对象这个对象是继承了原类的一个子类如下示意图:
子类是用字节码框架在运行时生成的使用Byte Buddy字节码框架处理技术生成的字节码字节码可以直接加载,选定一个合适的类加载器加载生成的字节码,并实例化为一个对象为什么要生成字节码呢,因为如果是生成源码还要先编译再加载,反而多了一个步骤。
从图上我们看到,mock对象处理的 *** 都交给mockHandler实例的handle *** 去处理,下面将详细分析这个对象和 *** 做了什么。
关系图
除了生成字节码,mock同时还做了一些很重要的事情,如下图生成了一些实例对象:
这些实例构成了mock和stub过程中的处理器和容器:
MockHandlerinvocationContainerstubbedmockHandler等对象
上面的图示过程可以在代码里找到印证,最主要的执行逻辑是在createMock *** 里面
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) { //生成并加载mock类 Class<? extends T> mockedProxyType = createMockType(settings); //实例化 T mockInstance = instantiator.newInstance(mockedProxyType); MockAccess mockAccess = (MockAccess) mockInstance; //给对象成员变量mockitoInterceptor赋值<-handler mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings)); return ensureMockIsAssignableToMockedType(settings, mockInstance); } ***代码在类MockUtil创建的handler: MockHandler mockHandler = createMockHandler(settings)在这个创建handler *** 的里面生成invocationContainer: this.invocationContainer = new InvocationContainerImpl(mockSettings)invocationContainer里面创建stubbed LinkedList<StubbedInvocationMatcher>用来存放打桩的函数和参数以及返回的结果。给对象成员变量mockitoInterceptor赋值为handler 这里,已经先通过Bytebuddy生成的字节码声明了mockitoInterceptor成员,这样再通过mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings))就给它赋值了。
Bytebuddy.builder()...//设置父类、名字 .method(matcher) .intercept(dispatcher) ...//设置其他 *** 属性,比如synchronized设置 //声明成员变量: mockitoInterceptor .defineField("mockitoInterceptor", MockMethodInterceptor.class, PRIVATE) ***代码
这样之后,mock对象里面的成员mockHander,通过getMockitoInterceptor函数能获取到,这个非常重要。
关系图增强
另外,MockingProgress是stub过程的工具类,在mock的过程中也生成了MockingProgress类的实例。可以看到:
public <T> T mock(Class<T> typeToMock, MockSettings settings) { ... T mock = createMock(creationSettings); mockingProgress().mockingStarted(mock, creationSettings); return mock; } ***代码mockingProgress()是生成一个ThreadLocal<MockingProgress>ThreadLocal表示线程的变量,可以支持多线程并发。
于是,现有的关系图增强了:
下面会具体分析MockingProgress的作用。
执行流程
假设线程thread1开始执行mock,执行流程如下图所示:
A a=Mockito.mock(A.class)初始化...Mockito.when(a.func(...)).then...,这里可以分解为如下三个流程调用
这是when里面的那一次调用:
a.func(“abc”) 执行了handle *** handle *** 里生成局部ongoingStubbing对象handler传递自己的成员invocationContainer给ongoingStubbinghandler将生成的ongoingStubbing对象push 到mockingProgress此时返回默认的empty结果null如下代码所示:
public Object handle(Invocation invocation) throws Throwable { OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer); mockingProgress().reportOngoingStubbing(ongoingStubbing); StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation); if (stubbing != null) { stubbing.captureArgumentsFrom(invocation); return stubbing.answer(invocation); } else { //使用的Mockito最初的withSettings()提供的默认返回值,null, //when里面的那一次调用就是返回的默认值 return mockSettings.getDefaultAnswer().answer(invocation); } } ***代码
when
when逻辑的执行:从当前线程的mockingProgress拉取pull对象ongoingStubbing
mockingProgress如果stubbingInProgress!=null,则前面的stub还没完成,将抛出异常如果stubbingInProgress=null,则设置stubbingInProgress,返回ongoingStubbing给when,设置自己的ongoingStubbing为null,when函数返回ongoingStubbingpublic <T> OngoingStubbing<T> when(T methodCall) { mockingProgress().stubbingStarted();//如果stubbingInProgress!=null,则前面的stub还没完成 return (OngoingStubbing<T>) mockingProgress.pullOngoingStubbing(); } ***代码
then
when函数返回的ongoingStubbing调用了then *** 。然后then从ongoingStubbing的成员invocationContainer拿到stubbed加锁并修改这个链表,也就是加入一个打桩的函数和参数以及返回的结果。通过invocationContainer可以知道,invoke送来的 *** 参数和取走后设置的返回值类型要对应,不然then执行会报错
再调用
最后使用stub的结果
a.func("abc"):从invocationContainer的stubbed匹配调用参数,能匹配则返回调用的结果MockingProgress引导状态演化
对于 MockingProgress来说
invoke 送来ongoingStubbing 可以送来多次,最后一次的为准when 取走ongoingStubbing 不能取走多次在执行各个操作的时候会判断ongoingStubbing是否为null
when 需要ongoingStubbing有值,之后then是ongoingStubbing调用的UnfinishedStubbingException stub过程 when-then是原子的,而且stub没有完成时调用a的 *** 或继续发起新的stub都会抛出Method threw 'org.mockito.exceptions.misusing.UnfinishedStubbingException' exception.MockingProgress再探究
小实验:多线程mock
两个线程同时mock的时候,相互不影响,因为stub过程是ThreadLocal的。 一个线程mock的数据,另外一个线程是可以使用的。
@Test void multiMockA() throws Exception{ A a = Mockito.mock(A.class); Mockito.when(a.func("one")).thenReturn(1); Thread thread = new Thread(() -> { Mockito.when(a.func("two")).thenReturn(2); System.out.println("a.func("one") = " + a.func("one")); }); thread.start(); thread.join(); System.out.println("a.func("two") = " + a.func("two")); } ***代码
返回
a.func("one") = 1 a.func("two") = 2 ***代码
这个小实验的目的是希望碰到了可以理解,但也不提倡这种用法。
小实验:一个线程里mock多个类
如下代码:
@Test void mocMultiClass() throws Exception{ A a = Mockito.mock(A.class); B b= Mockito.mock(B.class); Mockito.when(a.func("one")).thenReturn(1); Mockito.when(b.func(1)).thenReturn("one"); System.out.println("a.func("one") = " + a.func("one")); System.out.println("b.func(1) = " + b.func(1)); } ***代码
a.func("one") = 1 b.func(1) = one ***代码
这个正常操作都没有问题。 可以看出,MockingProgress帮着A stub 完,又帮着B mock,如下示意图:
不常用但可以了解的功能
最常用的就是上面详细描述的这种模式了,但也有一些不常用但可以了解的功能,主要是下面两种。
SPY
Spy是另外一种插桩的方式,和mock相比,字节码生成的时候interceptor处理的逻辑不同。
spy生成的对象,stub之前的 *** 调用(invoke),是调用原来的类(super)的 *** 处理逻辑。mock生成的对象,stub之前的 *** 调用是返回空值,比如null,***类则是空***。Mockito支持了几种***类的空值返回对于HashMap这样的返回类型,Mockito是返回一个空的***但是如果返回的是继承HashMap的类,比如NutMap,Mockito还是返回nullVERIFY模式
校验MOCK的 *** 是否调用过校验MOCK的 *** 调用次数等信息经验总结
总结了一些生产过程里遇到的几个案例分享,包括在SpringBoot里面使用Mock、 mock带泛型的对象时遇到的问题以及插桩的参数的一些问题。
SpringBoot和Mock
在Spingboot的测试里面,我们可以使用:
@MockBean 用于注入mock的单例对象@SpyBean 用于注入spy的单例对象mock和泛型的关系
实验一下@Test void mockGeneric(){ HashMap<String,Integer> list = Mockito.mock(HashMap.class); Mockito.when(list.get("one")).thenReturn(1); System.out.println("list.get("one") = " + list.get("one")); } ***代码
list.get("one") = 1 ***代码
这看起来也是完全可以的。
但是@MockBean的时候有问题,必须要用上泛型。 因为
@MockBean KafkaTemplate<String, Object> kafkaTemplate; ***代码
和
@MockBean KafkaTemplate<String, String> kafkaTemplate; ***代码
这两个不一样 因为KafkaTemplate<String, String> kafkaTemplate和 KafkaTemplate<String, Object> kafkaTemplate也是两个不一样的bean。
当然这不是Mockito的问题,而是Spring boot的问题。可以验证一下。
插桩的参数
参数包括两类:通常的参数和匹配器匹配器:可以使用Mockito.any的一系列函数来匹配参数,包括ArgumentMatchers类下面提供的所有静态 *** ,eq,startsWith等等。 *** 的多个参数使用模式要一致,否则会抛出InvalidUseOfMatchersException(参数匹配)异常,也就是如果有一个参数是采用匹配模式,另外一个不能使用固定模式(非ArgumentMatchers提供的)。链接:https://juejin.cn/post/7034528267721768990