使用Mockito修改Bean的依赖
概述
在使用单元测试时经常会遇到某些dependency依赖了外部资源,或者想主动绕过真正的方法执行mock返回结果而快速得到单元测试最终的期望结果,可能有以下两种场景,
对于TestCase A,设单元测试的方法是Service A的execute1方法和execute2方法,在执行execute1和execute2方法时都会调用ServiceB的不同方法,即ServiceA依赖了ServiceB;一个场景是完全对ServiceB进行Mock,如单元测试ServiceA#execute1方法时都通过Mock返回结果;一个场景是部分ServiceB的方法执行真实的业务逻辑(如查询数据库),一部分方法执行Mock返回结果,或Spy,如如单元测试ServiceA#execute2方法时,只mock ServiceB#b2结果,真正执行ServiceB#b1方法。
对TestCase的Service的依赖Bean的完全Mock
当对ServiceA的方法执行单元测试时,如ServiceA -> ServiceB,此时对ServiceB进行Mock,然后将其设置到ServiceA的属性中;后续ServiceA调用ServiceB的方法都降得到Mock后的结果;而对于ServiceB对象的本来的依赖本案暂且将其忽略,后续改进;
思路是在TestCase中依赖ServiceA的同时标示Mock ServiceB,待TestCase依赖注入完成后,新建ServiceB的Mock对象替换ServiceA中的ServiceB依赖;
@TestExecutionListeners({MockitoDependencyInjectionTestExecutionListener.class}) public class AServiceMockTest extends BaseTest { @Mock private BService bservice; @Autowired private AService aservice; @Before public void setup(){ doReturn("mock").when(bservice).b1(); } @Test public void test() { a.execute1(); } } @Service public class AServiceImpl implements AService { @Autowired private BService bservice; @Override public String execute1() { return bservice.b1(); //will return mock after Mock } }
当a.execute()执行时将调用aservice的属性bservice的b1方法,返回结果就是在setup方法中指定的结果;
监听TestCase的Service的依赖Bean
当对ServiceA进行单元测试时,依赖了ServiceB,需要获取ServiceB的b1方法的真正执行结果,Mock b2方法的结果,此时可以采用Spy方式;由于ServiceA依赖了ServiceB,而这个属性可能是个AopProxy对象,并不能直接使用Mockito.mock(bservice)或者Mockito.spy(bservice),所以这里@Spy注解指定的是实现类,通过MockitoDependencyInjectionTestExecutionListener处理后,获得一个Spy对象,同时这个Spy对象设置到bservice(AopProxy对象)中去;
@TestExecutionListeners({MockitoDependencyInjectionTestExecutionListener.class}) public class AServiceMockTest extends BaseTest { @Spy private BServiceImpl bserviceImpl; @Autowired private AService aservice; @Before public void setup(){ doReturn(true).when(bserviceImpl).b2(any(String.class)); } @Test public void test() { a.execute(); } } @Service public class AServiceImpl implements AService { @Autowired private BService bservice; @Override public boolean execute2() { String str = bservice.b1(); return bservice.b2(str); } }
MockitoDependencyInjectionTestExecutionListener的实现
public class MockitoDependencyInjectionTestExecutionListener extends DependencyInjectionTestExecutionListener { private Set<Field> injectFields = new HashSet<>(); private Map<String,Object> mockObjectMap = new HashMap<>(); @Override protected void injectDependencies(TestContext testContext) throws Exception { super.injectDependencies(testContext); init(testContext); } /** * when A dependences on B * mock B or Spy on targetObject of bean get from Spring IoC Container whose type is B.class or beanName is BImpl * @param testContext */ private void init(TestContext testContext) throws Exception { AutowireCapableBeanFactory factory =testContext.getApplicationContext().getAutowireCapableBeanFactory(); Object bean = testContext.getTestInstance(); Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { Annotation[] annotations = field.getAnnotations(); for (Annotation annotation : annotations) { if(annotation instanceof Mock){ Class<?> clazz = field.getType(); Object object = Mockito.mock(clazz); field.setAccessible(true); field.set(bean, object); mockObjectMap.put(field.getName(), object); } else if(annotation instanceof Spy) { Object fb = factory.getBean(field.getName()); //may be a proxy that can not be spy because $Proxy is final Object targetSource = AopTargetUtils.getTarget(fb); Object spyObject = Mockito.spy(targetSource); if (!fb.equals(targetSource)) { //proxy if (AopUtils.isJdkDynamicProxy(fb)) { setJdkDynamicProxyTargetObject(fb, spyObject); } else { //cglib setCglibProxyTargetObject(fb, spyObject); } } else { mockObjectMap.put(field.getName(), spyObject); } field.setAccessible(true); field.set(bean, spyObject); }else if (annotation instanceof Autowired){ injectFields.add(field); } } } for(Field field: injectFields) { field.setAccessible(true); Object fo = field.get(bean); if (AopUtils.isAopProxy(fo)) { Class targetClass = AopUtils.getTargetClass(fo); if(targetClass ==null) return; Object targetSource = AopTargetUtils.getTarget(fo); Field[] targetFields =targetClass.getDeclaredFields(); for(Field targetField : targetFields){ targetField.setAccessible(true); if(mockObjectMap.get(targetField.getName()) ==null){ continue; } ReflectionTestUtils.setField(targetSource,targetField.getName(), mockObjectMap.get(targetField.getName())); } } else { Object realObject = factory.getBean(field.getType()); if(null != realObject) { Field[] targetFields = realObject.getClass().getDeclaredFields(); for(Field targetField : targetFields){ targetField.setAccessible(true); if(mockObjectMap.get(targetField.getName()) ==null){ continue; } ReflectionTestUtils.setField(fo,targetField.getName(), mockObjectMap.get(targetField.getName())); } } } } } private void setCglibProxyTargetObject(Object proxy, Object spyObject) throws NoSuchFieldException, IllegalAccessException { Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0"); h.setAccessible(true); Object dynamicAdvisedInterceptor = h.get(proxy); Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised"); advised.setAccessible(true); ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).setTarget(spyObject); } private void setJdkDynamicProxyTargetObject(Object proxy, Object spyObject) throws NoSuchFieldException, IllegalAccessException { Field h = proxy.getClass().getSuperclass().getDeclaredField("h"); h.setAccessible(true); AopProxy aopProxy = (AopProxy) h.get(proxy); Field advised = aopProxy.getClass().getDeclaredField("advised"); advised.setAccessible(true); ((AdvisedSupport) advised.get(aopProxy)).setTarget(spyObject); } }
附
maven依赖
JUnit、Mockito

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Redis缓存失效机制
Redis缓存失效的故事要从EXPIRE这个命令说起,EXPIRE允许用户为某个key指定超时时间,当超过这个时间之后key对应的值会被清除,这篇文章主要在分析Redis源码的基础上站在Redis设计者的角度去思考Redis缓存失效的相关问题。 Redis缓存失效机制 Redis缓存失效机制是为应对缓存应用的一种很常见的场景而设计的,讲个场景: 我们为了减轻后端数据库的压力,很开心的借助Redis服务把变化频率不是很高的数据从DBload出来放入了缓存,因此之后的一段时间内我们都可以直接从缓存上拿数据,然而我们又希望一段时间之后,我们再重新的从DBload出当前的数据放入缓存,这个事情怎么做呢? 问题提出来了,这个问题怎么解决呢?好吧,我们对于手头的语言工具很熟悉,坚信可以很快的写出这么一段逻辑:我们记录上次从db load数据的时间,然后每次响应服务的时候都去判断时间是不是过期了,要不要从db重新load了……。当然这种方法也是可以的,然而当我们查阅Redis command document的时候,发现我们做了本来不需要做的事情,Redis本身提供这种机制,我们只要借助EXPIRE...
- 下一篇
dubbo源码解析-集群容错架构设计
前期铺垫 官网介绍图.png 这张是官网的对于集群容错的架构设计图,即使你有一定的使用经验,第一眼看到这个图可能还是有些懵逼.因为这个图是从设计的角度画出来的,而不是使用的角度.但是即使这个图你看不懂也不影响你对本文的阅读,但是你必须要记住三个关键词,因为这三个关键词接下来会贯穿全文,他们就是Directory,Router,LoadBalance 再接下来给大家一张"地图","地图"上我已经标记了序号,再下面的源码分析中,我也会实时提醒我们所在的位置,以至于不会迷失方向. 执行时序图.png 环境准备 既然是集群,那么首先要启动两个Provider,我这里是一个虚拟机,一个本地的方式,因为环境准备不是本文重点,因此略过.本文所用到的源码是2.5.4版本,可以在guihub上找到. 正式发车 这次示例选用的源码用dubbo-demo的dubbo-demo-consumer,如果对dubbo原理有些简单的了解就知道,他给接口注入的不是接口的实现类,而是一个代理类,如下图 接着自然是到了代理类的invoke方法里,从图中我们也可以看出,他用的是jdk的动态代理 下面要开始紧盯着地图了,他现...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS关闭SELinux安全模块
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2整合Redis,开启缓存,提高访问速度