zoukankan      html  css  js  c++  java
  • 教育单元测试mock框架优化之路(上)

    转载:https://sq.163yun.com/blog/article/169561874192850944
    众所周知,mock对于单元测试,尤其是基于spring容器的单元测试,是非常重要的。它可以帮助我们隔离其他模块的错误、外部依赖、慢操作、未实现功能等等,也可以帮助我们创建一些难以复现的场景等。
          教育这边目前选择的mock框架底层是基于Mockito的。因为Mockito具有使用起来简单,学习成本很低,API简洁,测试代码可读性高,无须录制mock方法就能返回默认值等优点。实际上,大名鼎鼎的springboot的test框架就是选择了mockito做为其底层实现,同时将其与junit4,spring完美的结合在一起。教育这边对单元测试的mock框架,也是站在这些巨人的肩膀上,同时针对碰到的问题,做了一系列的改造和优化。虽然框架改造仍有很多路要走,不过到目前为止碰到的问题和相关解决办法或思路,还是很值的拿出来和大家分享交流一下的。
     
    一、当@InjectMocks遇到动态代理
     
          可能大家会一个疑问,为什么不直接使用springboot的test框架?这里面主要有两个原因:
          1. 新框架的学习和迁移需要时间(这个听起来更像托词囧。的确,我们正在积极向springboot/cloud框架迁移中);
          2.springboot的test框架的默认实现中,对于@InjectMocks标注的属性,如果是动态代理对象时,其Inject注入的@Mock或@Spy对象,会被设置到proxy对象的属性上,而非target对象的属性上,这将导致mock失效。
    上面的情况听起来可能有点抽象,让我们来看一个实际的例子。
     
    图1. 一个Mockito的@Spy和@InejctMocks使用场景
     
     
    图2. 预期是注入userCouponService的spy代理对象,实际上是userCouponService的Spring AOP代理对象
     
     
    图3. bizOrderService的动态代理对象的同名属性,包含了一个userCouponService的spy代理对象
     
      
     
    图4. 测试实例中引用的userCouponService为spy代理对象,可以进行录制;BizOrderServiceImpl中
    userCouponService属性则不是spy代理对象,无法进行正常mock应答。 
    那么,是什么原因导致的呢?让我们来看下SpringBoot test框架中负责完成这一任务的 MockitoTestExecutionListener的实现。 
    public class MockitoTestExecutionListener extends AbstractTestExecutionListener {
            @Override
    	public void prepareTestInstance(TestContext testContext) throws Exception {
    		if (hasMockitoAnnotations(testContext)) {
                            //看这里
    			MockitoAnnotations.initMocks(testContext.getTestInstance());
    		}
    		injectFields(testContext);
    	}   

        可以看到Springboot的MockitoTestExecutionListener,在test实例化阶段,通过Mockito框架的MockitoAnnotations来对test实例进行mock注解扫描和注入。再具体跟进下MockitoAnnotations.initMocks实现内部,可以发现问题出现在其MockInjectionStrategy的实现上。我拿其中一种MockInjectionStrategy策略(Spy注入策略)来举例说明。

    public class SpyOnInjectedFieldsHandler extends MockInjectionStrategy {
        @Override
        protected boolean processInjection(Field field, Object fieldOwner, Set
           如上代码块,其中的fieldOwner为当前测试实例,field为当前测试类的orderService属性对象。因此,生成的mock(spy)对象,被注入到orderService的proxy对象,而非orderService的target对象。
           针对待InjectMocks的field对象如果是动态代理的情况,其实我们可以通过扩展Mockito框架的MockInjectionStrategy的processInjection方法,对相应用field的value进行unwrap代理后再进行InjectMock处理即可。遗憾的是,由于Mockito框架并发基于IOC设计,所以如何优雅的扩展,还需要后面再细细看下源码。当下我们是仿照了这个流程,自己完整的实现了一个spring test框架AbstractTestExecutionListener类来解决这个问题。下面是我们自定义的MockListener的主干实现。
    public class MockListener extends AbstractTestExecutionListener {
        @Override
        public void beforeTestMethod(TestContext testContext) throws Exception {
            testContext.setAttribute(TEST_CONTEXT_MOCKS_MAP_KEY, new HashMap());
            testContext.setAttribute(TEST_CONTEXT_SPYS_MAP_KEY, new HashMap());
            /*1. 为带@EduMock注解的属性创建Mock对象*/
            createMocks(testContext);
            /*2. 为带@Spy注解的属性创建Spy对象*/
            createSpys(testContext);
            /*3. 为带@EduInjectMocks注解的bean注入mock/spy对象*/
            injectIntoTargetBeans(testContext);
        }
        @Override
        public void afterTestMethod(TestContext testContext) throws Exception {
            /*还原为真实的对象*/
            Map> injectedBeanMap = getInjectedBeanMap(testContext);
            /*恢复bean依赖的真实对象*/
            for (Map.Entry> entry : injectedBeanMap.entrySet()){
                Object targetBean = entry.getKey();
                Map originalFieldMap = entry.getValue();
                unInjectTargetBean(targetBean, originalFieldMap);
            }
            testContext.removeAttribute(TEST_CONTEXT_MOCKS_MAP_KEY);
            testContext.removeAttribute(TEST_CONTEXT_SPYS_MAP_KEY);
            testContext.removeAttribute(TEST_CONTEXT_INJECTED_MAP_KEY);
        }
          这是其中injectIntoTargetBean部分的逻辑:
    private void injectIntoTargetBean(TestContext testContext, Field targetBeanField) throws Exception{
            Object testInst = testContext.getTestInstance();
            Object injectedBean = targetBeanField.get(testInst);     //被注入的目标Bean
            //看这里
            Object beanUnwrap = UnWrapProxyUtil.unwrapProxy(injectedBean);  //被spring代理的真实bean
            Class clazz = beanUnwrap.getClass();
            final Field[] fields = clazz.getDeclaredFields();      
            Map mockMap = (Map)testContext.getAttribute(TEST_CONTEXT_MOCKS_MAP_KEY);
            Map spyMap = (Map)testContext.getAttribute(TEST_CONTEXT_SPYS_MAP_KEY);
            
            Map originalFieldMap = new HashMap();
            for (Field field : fields){
                Object mock = mockMap.get(field.getName());
                Object spy = spyMap.get(field.getName());           
                if (null != mock || null != spy){
                    field.setAccessible(true);            
                    /*暂存目标bean原有的属性,测试类执行完后需要替换回原有属性*/
                    originalFieldMap.put(field, field.get(beanUnwrap));              
                    /*注入mock对象*/
                    if (null != mock){
                        ReflectionTestUtils.setField(beanUnwrap, field.getName(), mock);
                    }              
                    /*注入spy对象*/
                    if (null != spy){
                        ReflectionTestUtils.setField(beanUnwrap, field.getName(), spy);
                    }
                }
            }
            
            Map> injectedBeanMap = getInjectedBeanMap(testContext);
            injectedBeanMap.put(beanUnwrap, originalFieldMap);
        }
    private void injectIntoTargetBean(TestContext testContext, Field targetBeanField) throws Exception{
            Object testInst = testContext.getTestInstance();   
            Object injectedBean = targetBeanField.get(testInst);     //被注入的目标Bean
            //看这里
            Object beanUnwrap = UnWrapProxyUtil.unwrapProxy(injectedBean);  //被spring代理的真实bean        
            Class clazz = beanUnwrap.getClass();
            final Field[] fields = clazz.getDeclaredFields();      
            Map mockMap = (Map)testContext.getAttribute(TEST_CONTEXT_MOCKS_MAP_KEY);
            Map spyMap = (Map)testContext.getAttribute(TEST_CONTEXT_SPYS_MAP_KEY);       
            Map originalFieldMap = new HashMap();
            for (Field field : fields){
                Object mock = mockMap.get(field.getName());
                Object spy = spyMap.get(field.getName());          
                if (null != mock || null != spy){
                    field.setAccessible(true);               
                    /*暂存目标bean原有的属性,测试类执行完后需要替换回原有属性*/
                    originalFieldMap.put(field, field.get(beanUnwrap));              
                    /*注入mock对象*/
                    if (null != mock){
                        ReflectionTestUtils.setField(beanUnwrap, field.getName(), mock);
                    }            
                    /*注入spy对象*/
                    if (null != spy){
                        ReflectionTestUtils.setField(beanUnwrap, field.getName(), spy);
                    }
                }
            }       
            Map> injectedBeanMap = getInjectedBeanMap(testContext);
            injectedBeanMap.put(beanUnwrap, originalFieldMap);
        }

         可以看出来,我们自定义的MockListener与springboot test框架的MockitoTestExecutionListener的区别在于:

    1. 通过UnWrapProxyUtil.unwrapProxy对待InjectMocks的域值进行解除代理处理,获取实际的target对象来做InjectMocks。
    2. mock的实际和reset实现的方式不同。MockitoTestExecutionListener是在prepareTestInstance阶段做一次测试实例的mock注入,然后根据配置再每个测试方法执行前或后来进行所有mock对象的reset。我们自己实现的MockListener,是在每次测试方法执行前生成对应于的mock对象并完成注入,每次测试方法执行结束后,删除所有mock对象,并将标注有@InjectMocks的对象的所有的mock属性恢复为原始属性值。
    3.@Mock与@Spy注解的属性(功能)有缩减,不过Mockito提供出来的注解功能扩展基本也是鸡肋,不要也罢。
          总体来说,我们自己实现的基于注解的Mock属性注入机制,解决了待注入对象为动态代理对象时带来的问题,但在性能上,由于实现的简化(其实是偷工减料~),略有损失。不过对于微服务的单元测试来说,这点性能变化一般也不会产生有什么严重的后果。当然,后面还是会把mockito的MockInjectionStrategy策略的优雅扩展弄清楚。框架如果能更好,为什么不呢?
    二、 组件Bean如何优雅自动MOCK
     
         解决完上面这个问题后,我们仍然面临新问题:

    1.   需要手动配置大量的dubbo consumer、amqp template等mock bean的xml配置。

    图5. 大量的dubbo consumer的mock bean配置

    图6. 大量的amqp的mock bean配置

    2.  需要在产品端再额外单独定义(主要是因为namespace等属性不同)的Redis client,Memcache client,ZookeeperLockContext等bean的mock配置。

    图7. 由于namespace不同而需要定义多个JedisClient的Mock Bean

    图8. 由于beanName不同而需要定义多个ZookeeperLock的Mock Bean

    3.  由于需要将单元测试的bean的mock配置部分单独出来,导致单元测试基类的import管理混乱和不稳定。甚至导致业务bean定义的xml组织需要考虑单元测试mock的需求。这样,不仅导致配置复杂,业务设计受单元测试影响,又或导致业务和单元测试发生较大不一致。

    图9. 一个复杂的单元测试汇总xml配置文件

    针对上述问题,我们的解决思路是这样的:
    1. 业务Bean的组织方式不应收到单元测试mock实现方案的影响。Mock框架应该有能力在不破坏业务Bean的整体组织的基础上,根据需要动态的进行Bean组件的新增或替换。
    2. 自动替换的实现类应该与具体的mock需求、打桩实现方案分离。后面只要按需扩展不同的bean mock需求和打桩实现即可。
    3. mock匹配方案可以按照类型、bean名称、基类或接口来进行。
    4. mock对象或工厂类可以继承或过滤原bean的相关属性。
    5. 引入数据清理抽象。可以通过StubDataCleanListener来无差别的清理不同的Mock桩实现的内存状态数据。 
           因此,我基于Spring的 BeanDefinitionRegistryPostProcessor扩展点实现了做自动mock替换的EduMockAutoConfigBeanFactoryPostProcessor。该Processor会 根据自定义的@ AutoMock注解,对符合匹配规则的BeanDefinition 进行修改,将对应业务 BeanDefinition的beanClass 替换为的相应mock对象的BeanFactory类。此外,可以进一步通过属性匹配过滤器来传递或过滤bean属性到mock对象。 
          具体的EduMockAutoConfigBeanFactoryPostProcessor的 postProcessBeanDefinitionRegistry逻辑就是按照前面的设计思路实现的,我就不这里详细罗列了。不过,我们可以从从@ AutoMock和@ MockSetting这两个配置注解来了解该Processor支持的功能。
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoMock {
        Class[] byClass() default {};//根据类型进行匹配
        Class[] bySuperClass() default {};//根据基类进行匹配
        MockSetting[] byMockSetting() default {};//根据复杂的匹配或过滤配置
    }
     @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MockSetting{
          Class byClass() ;
          String propNameIncludeRegFilter() default ""; //符合正则匹配的属性会被传递
          String propNameExcludeRegFilter() default "";//符合正则匹配的属性会被过滤
          boolean copyContructArgs() default false;//传递构造函数参数
    }
         下面列一个dubbo consumer的Mock自动替换的Dubbo consumer的mock FactoryBean实现类。要增加一种组件的Mock替换是不是非常简单?~
    @Component
    @AutoMock(byMockSetting = @MockSetting(byClass = ReferenceBean.class,propNameIncludeRegFilter = "^interface$"))
    public class DubboReferenceMockFactoryBean extends ReferenceConfig implements FactoryBean<Object>{
    @Override
    public Object getObject() throws Exception {
    return PowerMockito.mock(this.getInterfaceClass());
    }
    @Override
    public Class<?> getObjectType() {
    return this.getInterfaceClass();
    }
    @Override
    public boolean isSingleton() {
    return true;
    }
    public String getVersion() {
    return null;
    }
    public void setVersion(String version) {
    }
    }

     

     
  • 相关阅读:
    MySQL性能优化的最佳经验
    18个网站SEO建议
    sql之left join、right join、inner join的区别
    PHP与MYSQL事务处理
    Firefox上Web开发工具库一览
    SphinxSE的安装
    python XML
    python yaml
    C语言文本处理
    Linux strace命令
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/9552500.html
Copyright © 2011-2022 走看看