文章目录
1 使用场景
在项目开发中,经常要依赖外部资源来进行,如数据库查询,请求第三方接口等等,如果数据库或者第三方接口挂了,就能等他们恢复才能进行单元测试;又如果想验证某些特定的场景,就需要通过造数据来达到某些特定的条件来,才能测试到特定的场景。
举个例子:当前有个拉取第三方商铺(如抖店、淘宝)的订单到本地订单服务入库的功能,现在想验证第三方商铺某个商品特定地址的订单能否正常入库,这时候就需要在第三方商铺下单相应的商品并填写相应的地址,然后才能跑单元测试验证。
这时候,我们是可以通过mock的方式,让查询外部资源的方法不用真正请求外部资源也能返回我们想要的结果。而mockito就是一个提供了这样的功能的框架。
2 mockito简单使用
mockito提供了很多功能,可以浏览官网进行学习,这里就从简单的hello world学起
Mockito官网
平时我们一般使用springboot框架,而spring-boot-starter-test模块就是集成了mockito。
注:本文使用的springboot版本是2.1.6.RELEASE,其集成mockito的版本是2.23.4
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><!-- <scope>test</scope> --></dependency>
下面是代码例子
publicclassMockTest{publicstaticvoidmain(String... args)throwsIOException{MockTest origin =newMockTest();System.out.println(origin.hello());System.out.println("------ 分割线 -------");MockTest mock =Mockito.mock(MockTest.class);when(mock.hello()).thenReturn(1,2);System.out.println(mock.hello());System.out.println(mock.hello());System.out.println(mock.hello());}publicinthello(){System.out.println("hello world");return0;}}
最终输出:
hello world
0
------ 分割线 -------
1
2
2
- 可以看到分割线前面是通过new的方式创建了对象,然后调用hello()方法输出了"hello world"并且返回了0;
- 接着分割线后面是通过mockito来mock了一个对象,并且通过when方法指定hello()方法依次返回"1,2",然后调用了3次hello()方法,依次返回了"1,2,2",可以看到并没有打印"hello world"字符串了
3 mockito原理
通过上面的例子,可以了解到mockito的一些功能,那它到底是怎样实现的呢,现在还是以上面的例子通过debug来一探它的源码。(注:本文mockito版本是2.23.4)
3.1 mock()方法原理
以Mockito.mock()方法为入口
publicstatic<T>Tmock(Class<T> classToMock){returnmock(classToMock,withSettings());}publicstatic<T>Tmock(Class<T> classToMock,MockSettings mockSettings){return MOCKITO_CORE.mock(classToMock, mockSettings);}
可以看到实际是通过MOCKITO_CORE.mock()来生成mock对象,MOCKITO_CORE是MockitoCore对象,继续进入其mock()方法查看
public<T>Tmock(Class<T> typeToMock,MockSettings settings){if(!MockSettingsImpl.class.isInstance(settings)){thrownewIllegalArgumentException("Unexpected implementation of '"+ settings.getClass().getCanonicalName()+"'\n"+"At the moment, you cannot provide your own implementations of that class.");}MockSettingsImpl impl =MockSettingsImpl.class.cast(settings);MockCreationSettings<T> creationSettings = impl.build(typeToMock);T mock =createMock(creationSettings);mockingProgress().mockingStarted(mock, creationSettings);return mock;}
可以看到先是构建好配置类对象MockCreationSetting,里面存放了一些实例化mock对象时需要用到的信息,然后调用createMock()方法生成mock对象
//类似spi机制,mockito会到resource文件夹下的mockito-extensions文件夹//去寻找是否有指定的MockMaker,没有的话,默认就是ByteBuddyMockMakerprivatestaticfinalMockMaker mockMaker =Plugins.getMockMaker();publicstatic<T>TcreateMock(MockCreationSettings<T> settings){//MockHandler是负责mock对象在方法执行时,进行相应的拦截处理MockHandler mockHandler =createMockHandler(settings);//默认是通过ByteBuddyMockMaker来创建mock对象T mock = mockMaker.createMock(settings, mockHandler);//这里判断如果是spy模式,且原对象实例不为空,就把原对象实例的成员变量值复制到mock对象Object spiedInstance = settings.getSpiedInstance();if(spiedInstance !=null){newLenientCopyTool().copyToMock(spiedInstance, mock);}return mock;}
上面可以看到,mock对象是通过MockMaker来创建了,由于默认是使用ByteBuddyMockMaker这个实现类,所以接下来看这个类的createMock()做了什么
privateClassCreatingMockMaker defaultByteBuddyMockMaker =newSubclassByteBuddyMockMaker();@Overridepublic<T>TcreateMock(MockCreationSettings<T> settings,MockHandler handler){return defaultByteBuddyMockMaker.createMock(settings, handler);}
可以看到,实际是调用SubclassByteBuddyMockMaker的createMock()来创创建mock对象,继续跟踪
public<T>TcreateMock(MockCreationSettings<T> settings,MockHandler handler){//创建mock对象的class对象,默认使用byteBuddy来生成Class<?extendsT> mockedProxyType =createMockType(settings);//找到一个实例初始化器,如果对象构造方法是无参的就是用objenesis库,如果是有参的就是用ConstructorInstantiator(通过筛选出确定的构造方法然后反射实例化)Instantiator instantiator =Plugins.getInstantiatorProvider().getInstantiator(settings);T mockInstance =null;try{//创建mock对象的class对象
mockInstance = instantiator.newInstance(mockedProxyType);//因为mock对象会实现MockAccess接口,可以强转MockAccess mockAccess =(MockAccess) mockInstance;//设置了拦截器,其实mock对象每个方法都是经过这个拦截器(aop的味道)
mockAccess.setMockitoInterceptor(newMockMethodInterceptor(handler, settings));returnensureMockIsAssignableToMockedType(settings, mockInstance);}catch(ClassCastException cce){...省略
}catch(org.mockito.creation.instance.InstantiationException e){thrownewMockitoException("Unable to create mock instance of type '"+ mockedProxyType.getSuperclass().getSimpleName()+"'", e);}}
看到这里基本知道了mockito创建mock对象的原理,就是在运行时,通过动态代理的形式,创建一个代理对象实例并返回。它是使用byteBuddy库生成子类的方式来实现的。
bytebuddy官网
3.2 when().thenReturn()原理
上面已经知道mock对象是通过byteBuddy库在运行时生成的class对象实例化的,那有没有办法看到mock对象的class源码呢?
arthas文档
这时候阿里的arthas就派上用场了,首先要保证程序保持运行(如加行代码:System.in.read();),然后启动arthas,选择对应的java程序,因为mock对象的class名称是带随机数的,需要先找出来是什么名称,可以使用sc命令搜索一下mock对象的class名称,最后在使用jad命令进行反编译即可;
[arthas@1656]$ sc com.test.example.* -E
com.test.example.MockTest
com.test.example.MockTest$MockitoMock$1912680993
com.test.example.MockTest$MockitoMock$1912680993$auxiliary$g1Dqkwtz
com.test.example.MockTest$MockitoMock$1912680993$auxiliary$lRJxdBSA
com.test.example.MockTest$MockitoMock$1912680993$auxiliary$v9vJMsdZ
Affect(row-cnt:5) cost in20 ms.
[arthas@1656]$ jad com.test.example.MockTest$MockitoMock$1912680993
可以看到多出了4个跟mock对象有关的4个class,其中MockTest$MockitoMock$1912680993就是mock对象的class
publicclassMockTest$MockitoMock$1912680993extendsMockTestimplementsMockAccess{...省略
publicinthello(){return(Integer)MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptSuperCallable(this,this.mockitoInterceptor, cachedValue$sDcnG1W8$4t661m1,newObject[0],newMockTest$MockitoMock$1912680993$auxiliary$v9vJMsdZ(this));}static{
cachedValue$sDcnG1W8$4t661m1 =MockTest.class.getMethod("hello",newClass[0]);}final/* synthetic */int hello$accessor$sDcnG1W8(){returnsuper.hello();}...省略
}
可以看到mock对象的方法,其实调用MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptSuperCallable()方法
publicstaticclassDispatcherDefaultingToRealMethod{@SuppressWarnings("unused")@RuntimeType@BindingPriority(BindingPriority.DEFAULT *2)publicstaticObjectinterceptSuperCallable(@ThisObject mock,@FieldValue("mockitoInterceptor")MockMethodInterceptor interceptor,@OriginMethod invokedMethod,@AllArgumentsObject[] arguments,@SuperCall(serializableProxy =true)Callable<?> superCall)throwsThrowable{if(interceptor ==null){return superCall.call();}return interceptor.doIntercept(
mock,
invokedMethod,
arguments,newRealMethod.FromCallable(superCall));}}
该方法中interceptor,其实是前文出现过的MockMethodInterceptor
mockAccess.setMockitoInterceptor(new MockMethodInterceptor(handler, settings));
ObjectdoIntercept(Object mock,Method invokedMethod,Object[] arguments,RealMethod realMethod)throwsThrowable{returndoIntercept(
mock,
invokedMethod,
arguments,
realMethod,newLocationImpl());}ObjectdoIntercept(Object mock,Method invokedMethod,Object[] arguments,RealMethod realMethod,Location location)throwsThrowable{return handler.handle(createInvocation(mock, invokedMethod, arguments, realMethod, mockCreationSettings, location));}
可以看到,最终是调用了handler的handle方法返回最终结果,handler再创建时用了装饰者模式,真正干活的是MockHandlerImpl
publicclassMockHandlerImpl<T>implementsMockHandler<T>{...省略
publicObjecthandle(Invocation invocation)throwsThrowable{...省略
InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(mockingProgress().getArgumentMatcherStorage(),
invocation
);...省略
// prepare invocation for stubbing
invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);OngoingStubbingImpl<T> ongoingStubbing =newOngoingStubbingImpl<T>(invocationContainer);//保存存根mockingProgress().reportOngoingStubbing(ongoingStubbing);// look for existing answer for this invocationStubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);...if(stubbing !=null){
stubbing.captureArgumentsFrom(invocation);//如果用了when方法,就可能匹配上对应的answer,从而返回我们想要的结果return stubbing.answer(invocation);}else{//使用默认的answer返回结果Object ret = mockSettings.getDefaultAnswer().answer(invocation);...省略
//返回结果return ret;}}}
可以看到,主要是在invocationContainer里面找出当前调用方法匹配到的answer实例(存根),如果匹配不到就用默认的answer实例,然后调用answer实例的answer方法获得返回值。
通过一步一步debug可以知道,when().thenReturn()就是往invocationContainer里面添加了一个StubbedInvocationMatcher,所以在MockHandlerImpl的handle方法里会被匹配上,返回我们指定的值。
4 sprinboot结合junit使用mockito进单元测试
4.1 单元测试例子
先简单写个bean
@ComponentpublicclassTestBean{publicintsayHello(){System.out.println("hello world");return0;}}
再简单写个单元测试
@SpringBootTest@RunWith(SpringRunner.class)publicclassMockTest{@MockBeanTestBean testBean;@Testpublicvoidtest(){when(testBean.sayHello()).thenReturn(1);System.out.println(testBean.sayHello());}}
其中@MockBean注解是springboot提供的,加上这个注解,spring容器中该对象就是一个mock对象了,所以注入的对象也是一个mock对象。
4.2 @MockBean注解的原理
那@MockBean是如何把spring容器的对象,修改成mock对象的呢?
这时候可以在idea点击进入@MockBean的源码,然后再查看@MockBean在哪些类中被使用了,这时候会找到一个MockitoPostProcessor 类
publicclassMockitoPostProcessorextendsInstantiationAwareBeanPostProcessorAdapterimplementsBeanClassLoaderAware,BeanFactoryAware,BeanFactoryPostProcessor,Ordered{...省略
@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throwsBeansException{Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,"@MockBean can only be used on bean factories that "+"implement BeanDefinitionRegistry");postProcessBeanFactory(beanFactory,(BeanDefinitionRegistry) beanFactory);}privatevoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory,BeanDefinitionRegistry registry){
beanFactory.registerSingleton(MockitoBeans.class.getName(),this.mockitoBeans);DefinitionsParser parser =newDefinitionsParser(this.definitions);for(Class<?> configurationClass :getConfigurationClasses(beanFactory)){
parser.parse(configurationClass);}//拿出所有需要替换成mock对象的类Set<Definition> definitions = parser.getDefinitions();for(Definition definition : definitions){Field field = parser.getField(definition);//开始修改成mock对象,并注册到bean工厂register(beanFactory, registry, definition, field);}}privatevoidregister(ConfigurableListableBeanFactory beanFactory,BeanDefinitionRegistry registry,Definition definition,Field field){if(definition instanceofMockDefinition){//是@MockBean注解就会进到这里registerMock(beanFactory, registry,(MockDefinition) definition, field);}elseif(definition instanceofSpyDefinition){//是@SpyBean注解就会进到这里registerSpy(beanFactory, registry,(SpyDefinition) definition, field);}}privatevoidregisterMock(ConfigurableListableBeanFactory beanFactory,BeanDefinitionRegistry registry,MockDefinition definition,Field field){//创建bean工厂接受的RootBeanDefinition RootBeanDefinition beanDefinition =createBeanDefinition(definition);String beanName =getBeanName(beanFactory, registry, definition, beanDefinition);String transformedBeanName =BeanFactoryUtils.transformedBeanName(beanName);if(registry.containsBeanDefinition(transformedBeanName)){//拿到原先的BeanDefinitionBeanDefinition existing = registry.getBeanDefinition(transformedBeanName);copyBeanDefinitionDetails(existing, beanDefinition);//把它从bean工厂中删除
registry.removeBeanDefinition(transformedBeanName);}//替换成mock对象的BeanDefinition
registry.registerBeanDefinition(transformedBeanName, beanDefinition);//创建mock对象Object mock = definition.createMock(beanName +" bean");//注册mock对象实例到bean工厂,这样bean工厂在获取的时候就不用再走bean的创建流程
beanFactory.registerSingleton(transformedBeanName, mock);this.mockitoBeans.add(mock);this.beanNameRegistry.put(definition, beanName);if(field !=null){this.fieldRegistry.put(field, beanName);}}}
可以看到它实现了BeanFactoryPostProcessor,所以在bean实例化之前可以对bean工厂进行加工(spring容器启动流程可参考AbstractApplicationContext类的refresh方法),从而就有机会修改bean工厂里面的bean了。
从上面的代码也可以看到,它的做法就是把bean工厂里旧的对象的BeanDefinition删掉,然后替换成mock对象的BeanDefinition,接着调用definition.createMock(beanName + " bean")来实例化一个mock对象,再注册到bean工厂里,这样bean工厂在获取的时候就不用再走bean的创建流程,所以我们拿到的就是一个mock对象。
接下来看下definition.createMock()方法是如何创建一个mock对象实例的
public<T>TcreateMock(String name){MockSettings settings =MockReset.withSettings(getReset());if(StringUtils.hasLength(name)){
settings.name(name);}if(!this.extraInterfaces.isEmpty()){
settings.extraInterfaces(ClassUtils.toClassArray(this.extraInterfaces));}
settings.defaultAnswer(this.answer);if(this.serializable){
settings.serializable();}return(T)Mockito.mock(this.typeToMock.resolve(), settings);}
可以看到最终是调用Mockito.mock()方法生成的一个实例,它的源码在文章前面已分析过了。
4.3 MockitoPostProcessor是如何注册到spring容器的
到这里,笔者还有个疑问,BeanFactoryPostProcessor是需要注册到spring容器才能被触发的,然而MockitoPostProcessor类上并没有类似@Component等注解,那spring是如何扫描到它,并注册到容器的呢?
这时候,还是可以通过idea点击MockitoPostProcessor类,查看其被引用的地方,这时可以找到MockitoContextCustomizer类
classMockitoContextCustomizerimplementsContextCustomizer{privatefinalSet<Definition> definitions;MockitoContextCustomizer(Set<?extendsDefinition> definitions){this.definitions =newLinkedHashSet<>(definitions);}@OverridepublicvoidcustomizeContext(ConfigurableApplicationContext context,MergedContextConfiguration mergedContextConfiguration){if(context instanceofBeanDefinitionRegistry){//把MockitoPostProcessor的BeanDefinition注册到bean工厂MockitoPostProcessor.register((BeanDefinitionRegistry) context,this.definitions);}}...省略
}
可以看到时在MockitoContextCustomizer类的customizeContext方法,MockitoPostProcessor的BeanDefinition注册到bean工厂。而再跟踪customizeContext方法发现是在ContextCustomizerAdapter类调用的。
privatestaticclassContextCustomizerAdapterimplementsApplicationContextInitializer<ConfigurableApplicationContext>{privatefinalContextCustomizer contextCustomizer;privatefinalMergedContextConfiguration config;ContextCustomizerAdapter(ContextCustomizer contextCustomizer,MergedContextConfiguration config){this.contextCustomizer = contextCustomizer;this.config = config;}@Overridepublicvoidinitialize(ConfigurableApplicationContext applicationContext){this.contextCustomizer.customizeContext(applicationContext,this.config);}}
可以看到它实现了ApplicationContextInitializer接口,实现这个接口,就可以在springboot刷新容器之前,有机会对容器做加工处理(具体流程可以看SpringApplication类的run方法,其中prepareContext步骤就会触发所有ApplicationContextInitializer)
通过debug可以看到ContextCustomizerAdapter在什么时候添加到springboot的,限于篇幅,这里直接说结论。
结论:springboot的test模块在启动时,会创建一个springboot的上下文,会去加载META-INF/spring.factories文件中的实现了ContextCustomizerFactory接口的类,并调用这个工厂的接口创建ContextCustomizer ,然后加入到springboot的上下文中,从而springboot启动的时候,就会触发到。其中MockitoContextCustomizerFactory就是用来创建MockitoContextCustomizer的
classMockitoContextCustomizerFactoryimplementsContextCustomizerFactory{@OverridepublicContextCustomizercreateContextCustomizer(Class<?> testClass,List<ContextConfigurationAttributes> configAttributes){// We gather the explicit mock definitions here since they form part of the// MergedContextConfiguration key. Different mocks need to have a different key.DefinitionsParser parser =newDefinitionsParser();
parser.parse(testClass);//创建returnnewMockitoContextCustomizer(parser.getDefinitions());}}
4.4 带@MockBean注解的成员变量如何注入
我们常用的@Autowired、@Resource注解来注入成员变量,是通过bean后置处理器来解析并注入。那带@MockBean注解的成员变量是如何注入的呢?
还是在idea通过crtl+鼠标左键点击@MockBean注解,发现其被引用的地方有个类叫MockitoTestExecutionListener,就是由它来解析注入的。
可以看到,它实现了TestExecutionListener接口,其中prepareTestInstance()方法在测试用例执行之前会被调用(这里涉及到juit框架和SpringJUnit4ClassRunner,限于篇幅不展开讲)。
publicclassMockitoTestExecutionListenerextendsAbstractTestExecutionListener{@OverridepublicvoidprepareTestInstance(TestContext testContext)throwsException{//解析带有mockito框架提供的注解的成员变量initMocks(testContext);//解析带有spring框架提供的@MockBean注解的成员变量injectFields(testContext);}privatevoidinjectFields(TestContext testContext){//实际是通过MockitoPostProcessor的inject方法来注入postProcessFields(testContext,(mockitoField, postProcessor)-> postProcessor.inject(mockitoField.field,
mockitoField.target, mockitoField.definition));}privatevoidpostProcessFields(TestContext testContext,BiConsumer<MockitoField,MockitoPostProcessor> consumer){DefinitionsParser parser =newDefinitionsParser();//扫描出带有@MockBean等注解的成员变量
parser.parse(testContext.getTestClass());if(!parser.getDefinitions().isEmpty()){//从spring容器中拿到MockitoPostProcessor MockitoPostProcessor postProcessor = testContext.getApplicationContext().getBean(MockitoPostProcessor.class);for(Definition definition : parser.getDefinitions()){Field field = parser.getField(definition);if(field !=null){
consumer.accept(newMockitoField(field, testContext.getTestInstance(), definition), postProcessor);}}}}}
可以看到实际是通过MockitoPostProcessor的inject方法来注入的
voidinject(Field field,Object target,Definition definition){String beanName =this.beanNameRegistry.get(definition);Assert.state(StringUtils.hasLength(beanName),()->"No bean found for definition "+ definition);inject(field, target, beanName);}privatevoidinject(Field field,Object target,String beanName){try{
field.setAccessible(true);Assert.state(ReflectionUtils.getField(field, target)==null,()->"The field "+ field +" cannot have an existing value");//从spring容器中拿出实例Object bean =this.beanFactory.getBean(beanName, field.getType());//反射赋值ReflectionUtils.setField(field, target, bean);}catch(Throwable ex){thrownewBeanCreationException("Could not inject field: "+ field, ex);}}
可以看到,最终是从spring容器中拿出mock实例,然后通过反射赋值
5 总结
- mockito能解决对复杂的类依赖和调用关系的场景单元测试的困难
- mockito的主要原理是动态代理,在运行时生成子类
- springboot的@MockBean注解原理是实现BeanFactoryPostProcessor接口,在bean实例化之前,替换成mock对象beanDefinition,并通过mockito创建mock对象实例注册到BeanFactory
版权归原作者 seasonLai 所有, 如有侵权,请联系我们删除。