转载: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) {
}
}

 

 

教育单元测试mock框架优化之路(上)的更多相关文章

  1. 教育单元测试mock框架优化之路(下)

    转载:https://sq.163yun.com/blog/article/169563599967031296 四.循环依赖的解决 果然! 当我将@SpyBean应用到存在有循环依赖的Bean上时, ...

  2. 教育单元测试mock框架优化之路(中)

    转载:https://sq.163yun.com/blog/article/169564470918451200 三.间接依赖的bean的mock替换 对于前面提供的@Mock,@Spy+@Injec ...

  3. 单元测试mock框架——jmockit实战

    JMockit是google code上面的一个java单元测试mock项目,她很方便地让你对单元测试中的final类,静态方法,构造方法进行mock,功能强大.项目地址在:http://jmocki ...

  4. 单元测试Mock框架Powermockito 【mockito1.X】

    <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> &l ...

  5. 新浪微博iOS客户端架构与优化之路

    新浪微博iOS客户端架构与优化之路   随着Facebook.Twitter.微博的崛起,向UGC.PGC.OGC,自媒体提供平台的内 容消费型App逐渐形成了独特的客户端架构模式.与电商和通讯工具类 ...

  6. 阿里巴巴 web前端性能优化进阶路

    Web前端性能优化WPO,相信大多数前端同学都不会陌生,在各自所负责的站点页面中,也都会或多或少的有过一定的技术实践.可以说,这个领域并不缺乏成熟技术理论和技术牛人:例如Yahoo的web站点性能优化 ...

  7. 单元测试及框架简介 --junit、jmock、mockito、powermock的简单使用

    转 单元测试及框架简介 --junit.jmock.mockito.powermock的简单使用 2013年08月28日 14:33:06 luvinahlc 阅读数:6413 标签: 测试工具单元测 ...

  8. 微博MySQL优化之路--dockone微信群分享

    微博MySQL优化之路 数据库是所有架构中不可缺少的一环,一旦数据库出现性能问题,那对整个系统都回来带灾难性的后果.并且数据库一旦出现问题,由于数据库天生有状态(分主从)带数据(一般还不小),所以出问 ...

  9. MoQ(基于.net3.5,c#3.0的mock框架)简单介绍

    我们在做单元测试的时候,常常困扰于数据的持久化问题,很多情况下我们不希望单元测试影响到数据库中的内容,而且受数据库的影响有时我们的单元测试的速度会很慢,所以我们往往希望将持久化部分隔离开,做单元测试的 ...

随机推荐

  1. Linux kernel AIO

    http://blog.csdn.net/abcd1f2/article/details/47440087

  2. CSS高速制作图片轮播的焦点

    来源:http://www.ido321.com/858.html 效果图: 演示地址:http://jsfiddle.net/Web_Code/q5qfd8aL/embedded/result/ 代 ...

  3. dbMigration .NET 数据同步迁移工具

    官网:http://fishcodelib.com/DBMigration.htm

  4. 从Web Service和Remoting Service引出WCF服务

    本篇先通过Web Service和Remoting Service创建服务,抛砖引玉,再体验WCF服务.首先一些基本面: 什么是WCF? Windows Communication Foundatio ...

  5. C++笔记:头文件的作用和写法

    from://http://ceeji.net/blog/c%E7%AC%94%E8%AE%B0%EF%BC%9A%E5%A4%B4%E6%96%87%E4%BB%B6%E7%9A%84%E4%BD% ...

  6. 基于Memcached的tomcat集群session共享所用的jar

    多个tomcat各种序列化策略配置如下:一.java默认序列化tomcat配置conf/context.xml添加<Manager className="de.javakaffee.w ...

  7. MYSQL中group_concat有长度限制!默认1024(转载)

    在mysql中,有个函数叫“group_concat”,平常使用可能发现不了问题,在处理大数据的时候,会发现内容被截取了,其实MYSQL内部对这个是有设置的,默认不设置的长度是1024,如果我们需要更 ...

  8. [Android Security] Smali和逆向分析

    copy : https://blog.csdn.net/u012573920/article/details/44034397 1.Smali简介 Smali是Dalvik的寄存器语言,它与Java ...

  9. [A类会议] 国内论文检索

    https://www.cn-ki.net/ http://www.koovin.com

  10. Kubernetes中StatefulSet介绍

    StatefulSet 是Kubernetes1.9版本中稳定的特性,本文使用的环境为 Kubernetes 1.11.如何搭建环境可以参考kubeadm安装kubernetes V1.11.1 集群 ...