转载:https://sq.163yun.com/blog/article/169564470918451200

三、间接依赖的bean的mock替换

对于前面提供的@Mock,@Spy+@InjectMocks的方案,通过了解其源码实现可以发现,存在无法解决间接依赖bean的mock替换问题。还是拿前面的OrderService和UserCouponService举例:

如果OrderService的实现类OrderServiceImpl直接依赖UserCouponService,所以可以通过上述方案实现注入。但设想下如下场景:

case1.  OrderServiceImpl并没有直接依赖UserCouponService,而是间接依赖一个MarketService。然后MarkService的实现类MarkServiceImpl再依赖了UserCouponService。

case2. OrderServiceImpl同时直接和间接依赖UserCouponService。

这种场景下,我们发现OrderServiceImpl中直接依赖的UserCouponService已经被替换为Mock对象,而依赖MarkService对象中UserCouponService则不是Mock对象。到这里,问题已经很明显了。@Mock,@Spy+@InjectMocks所实现的方案,只完成了当前测试实例中所有标注了@InjectMocks属性对象中所直接依赖的属性的mock输入。而间接依赖的属性就爱莫能助了。

这个问题我们该如何解决?

首先,持有纯正的单元测试理念的同学可能会先跳出来。"单元测试,应该尽可能的细化测试的粒度。像上面的情况,应该mock整个MarketService才对,就没有Mock MarkServiceImpl中的UserCouponService的必要了"。是的,这理念的确是对的。理想比较美好,但现实可能就比较骨干了。因为毕竟我们限制了单元测试编写者使用Mock的灵活性。可能因此无法做一些业务逻辑粒度虽然稍微粗一点,但逻辑含义会相对比较完整,同时输入输出更为简单的"原子业务"粒度的测试。事实上,当我们开始选择使用spring test框架时,我们就已经走在这条路上了。

因此,我们需要支持间接依赖的bean的mock替换。那么,我们能否在原先的MockTestExecutionListener的实现中,增加多层嵌套的Inject呢?显然答案也是明确的,这等同于要实现一个网状遍历,根本不靠谱。话说回来,我们为什么不通过DI来实现?

如果大家看过Springboot test框架的源码实现,相信也会发现它已经在其中增加了@MockBean和@SpyBean的设计方案。这套方式使得开发者可以通过在测试类的属性上增加上述注解的方式,将Spring BeanFactory中对应类型的bean替换为Mock或Spy对象。这样所有符合条件的直接或间接依赖的属性bean,都会被mock或spy掉。让我们来具体看看Springboot test框架在这方面的具体实现。首先,我们看下对应jar包中的spring.factories文件。

# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.boot.test.context.ImportsContextCustomizerFactory,\
org.springframework.boot.test.context.SpringBootTestContextCustomizerFactory,\
org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\
org.springframework.boot.test.mock.mockito.MockitoContextCustomizerFactory # Test Execution Listeners
org.springframework.test.context.TestExecutionListener=\
org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener,\
org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener

其中值得重点关注的,主要是MockitoContextCustomizerFactory以及MockitoTestExecutionListener和ResetMocksTestExecutionListener这两个

TestExecutionListener。玄机尽在其中。先看下其中MockitoContextCustomizerFactory的实现:

class MockitoContextCustomizerFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(Class testClass,
List 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 = new DefinitionsParser();
parser.parse(testClass);
return new MockitoContextCustomizer(parser.getDefinitions());
}
}

可以看到MockitoContextCustomizerFactory主要依赖了一个DefinitionsParser,后者会解析当前testClass及其父类中的所有@MockBean和@SpyBean注解信息,解析后将相关MockDefition传递给MockitoContextCustomizer。再来看看MockitoContextCustomizer的实现:

class MockitoContextCustomizer implements ContextCustomizer {
private final Set<Definition> definitions;

   MockitoContextCustomizer(Set<? extends Definition> definitions) {
this.definitions = new LinkedHashSet<Definition>(definitions);
} @Override
public void customizeContext(ConfigurableApplicationContext context,
MergedContextConfiguration mergedContextConfiguration) {
if (context instanceof BeanDefinitionRegistry) {
         //看这里
MockitoPostProcessor.register((BeanDefinitionRegistry) context,
this.definitions);
}
}

从上面的源码可以看到,MockitoContextCustomizer是一个Spring初始化扩展ContextCustomizer的实现类,它会在spring容器初始化阶段,往

BeanDefinitionRegistry里添加MockitoPostProcessor的BeanDefinition(事实上还会包含一个SpyPostProcessor,后面也有关于它的相关介绍)。其中,MockitoPostProcessor是一个同时集成InstantiationAwareBeanPostProcessorAdapter和实现了BeanFactoryPostProcessor接口的类。接下来,对Spring的各种Processor的生命周期或作用阶段不太清楚的同学可能要先去补补功课。然后,让我们来看下这家伙都在干什么:

首先,在BeanFactoryPostProcessor.postProcessBeanFactory阶段:

1.为所有注解了@MockBean的属性创建Mock对象,注册到BeanFactory中,同时往BeanDefinitionRegistry添加相应的BeanDefinition。

2.为所有注解了@SpyBean的属性创建Spy相关的BeanDefinition,添加到BeanDefinitionRegistry,同时维护SpyDefinition、beanName、测试实例对应的Field的关系信息。

这里大家可能和我一样,会有一些疑问:

1.为什么@SpyBean和@MockBean在这个阶段有这些不同呢?

相信大家都已经想到,因为Spy的CallRealMethod设计,导致其需要依赖target bean。所以其Spy代理对象的生成,会延后到后面SpyPostProcessor进行创建。

2.MockitoPostProcessor为什么需要继承InstantiationAwareBeanPostProcessorAdapter呢?

阅读其源码可以发现,MockitoPostProcessor只重载了postProcessPropertyValues方法,它会在属性Inject阶段,将所有的MockBean和SpyBean对象注入到测试实例中。

讲完了MockBean对象的生成、注册和注入,该到SpyBean了。SpyBean到测试实例的注入,前面讲过,是在MockitoPostProcessor的postProcessPropertyValues阶段。因此,MockitoPostProcessor只负责SpyBean的创建和注册,这个阶段是不会真正去创建Spy对象的。让我们来具体看下SpyPostProcessor的实现:

static class SpyPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements PriorityOrdered { private static final String BEAN_NAME = SpyPostProcessor.class.getName();
private final MockitoPostProcessor mockitoPostProcessor;
SpyPostProcessor(MockitoPostProcessor mockitoPostProcessor) {
this.mockitoPostProcessor = mockitoPostProcessor;
} @Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
} @Override
public Object getEarlyBeanReference(Object bean, String beanName)
throws BeansException {
return createSpyIfNecessary(bean, beanName);
} @Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof FactoryBean) {
return bean;
}
return createSpyIfNecessary(bean, beanName);
}

可以看到,SpyPostProcessor继承了InstantiationAwareBeanPostProcessorAdapter,并实现了PriorityOrdered接口。在getEarlyBeanReference

和postProcessAfterInitialization阶段,SpyPostProcessor都去做了createSpyIfNecessary,即创建spy代理对象。而其中具体的创建逻辑,是由

SpyDefinition类代理的。那我们再看下SpyDefinition的实现:

public  T createSpy(String name, Object instance) {
Assert.notNull(instance, "Instance must not be null");
Assert.isInstanceOf(this.typeToSpy.resolve(), instance);
if (this.mockUtil.isSpy(instance)) {
return (T) instance;
}
MockSettings settings = MockReset.withSettings(getReset());
if (StringUtils.hasLength(name)) {
settings.name(name);
}
settings.spiedInstance(instance);
settings.defaultAnswer(Mockito.CALLS_REAL_METHODS);
return (T) Mockito.mock(instance.getClass(), settings);
}

看到这里,是否已经全然明了?~

是的,我当时也是这么觉得,开开心心的run it!然后,就开始被一系列的问题打击......

首先,第一个问题是,我发现当@SpyBean标记的属性是一个符合Spring的AbstractAutoProxyCreator代理条件的bean时,其属性值竟然不是Spy代理对象,而是一个对象名称类似于XXXEnhancerByMockitoWithCGLIB@dddEnhancerBySpringCGLIB@eee的东东。不过,这个问题显而易见。应该是委托bean先被Mockito代理,再被spring AutoProxy代理。这就导致了这个"伪Mock"对象根本就无法正常录制脚本。

那么,是否这个顺序应该反过来才是合理呢?

于是乎自定义SpyPostProcessor的实现,使其优先级低于AbstractAutoProxyCreator。果然,这招还挺有效,生成的spy对象名称已经成功的换成预期的XXXEnhancerBySpringCGLIB@eeeEnhancerByMockitoWithCGLIB@ddd。同时,观察其他bean对其的依赖,也都全部更新为其spy代理的实例,且对象地址相同。MS大功告成!~

然而悲剧很快上演。这个时候我悲催的发现,连录制都失败了。追踪其行为发现,虽然该对象名称已经改XXXEnhancerBySpringCGLIB@eeeEnhancerByMockitoWithCGLIB@ddd。但对其的调用,只会切入到Spring Aop的拦截部分,而Mockito代理的MethodInvokation拦截部分根本没有进入。观察下该bean对象的属性,发现其除了代表SpringAOP代理拦截的CGLIBCALLBAK0 6之外,另外还有代表Mockito代理拦截的CGLIBCALLBAK0~1。CALLBAK0~1这两个属性,丫的名称一样,导致虚拟机从上往下调用时,只处理了优先增强的Spring AOP拦截器CGLIB$CALLBAK0~1。

这是两次动态代理导致的问题吗?

还真不是!因为我记得之前代理顺序反过来的时候,SpringAOP代理对象中,并没有由于Mockito代理而生成的CGLIB$CALLBAK0~1属性。当然,其target对象的确是Mockito代理对象。那为什么将代理顺序反过来时,会这么奇怪?

跟踪下Mockito的mock代理实现,发现这家伙干了件很不光彩的事情。我们看下其中关键的MockUtil. createMock代码部分:

public class MockUtil {

    private static final MockMaker mockMaker = Plugins.getMockMaker();

    public boolean isTypeMockable(Class type) {
return !type.isPrimitive() && !Modifier.isFinal(type.getModifiers());
} public T createMock(MockCreationSettings settings) {
MockHandler mockHandler = new MockHandlerFactory().create(settings); T mock = mockMaker.createMock(settings, mockHandler); Object spiedInstance = settings.getSpiedInstance();
if (spiedInstance != null) {
new LenientCopyTool().copyToMock(spiedInstance, mock);
} return mock;
}

如上源码所示,在mock代理对象创建出来后,发现当前是spy场景时,会将spyInstance中的所有属性,copy到mock代理对象中。这就导致了SpringAOP代理的CGLIBCALLBAK0 6等属性也被拷贝过来,然后在调用顺序上截杀了Mockito注入的CGLIBCALLBAK0~1属性,阿门!

既然凶手已经找到,问题也总应该能够得到解决。看到这里,相信大家和我一样会有一个疑问,为什么SpyBean的代理对象,需要从target对象全量copy属性?Mockito在创建Spy对象时,不是已经在MockSettingImpl中增加了spiedInstance的引用了吗,难道它没有在CallRealMethod的实现中,将请求路由到spiedInstance的相关方法的invoke中去吗?

有此疑问,那我们就看下它内部的实现原理。首先看下Mockito代理对象的拦截行为MethodInterceptorFilter的实现:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
if (objectMethodsGuru.isEqualsMethod(method)) {
return proxy == args[0];
} else if (objectMethodsGuru.isHashCodeMethod(method)) {
return hashCodeForMock(proxy);
} else if (acrossJVMSerializationFeature.isWriteReplace(method)) {
return acrossJVMSerializationFeature.writeReplace(proxy);
} MockitoMethodProxy mockitoMethodProxy = createMockitoMethodProxy(methodProxy);
new CGLIBHacker().setMockitoNamingPolicy(methodProxy); MockitoMethod mockitoMethod = createMockitoMethod(method); CleanTraceRealMethod realMethod = new CleanTraceRealMethod(mockitoMethodProxy);
Invocation invocation = new InvocationImpl(proxy, mockitoMethod, args, SequenceNumber.next(), realMethod);
return handler.handle(invocation);
}

可以发现,拦截器的所有调用后面会被传递到InvocationImpl的mockitoMethod或realMethod上。那我们再看下MockHandlerImpl类的的相关实现:

// look for existing answer for this invocation
StubbedInvocationMatcher stubbedInvocation = invocationContainerImpl.findAnswerFor(invocation); if (stubbedInvocation != null) {
stubbedInvocation.captureArgumentsFrom(invocation);
return stubbedInvocation.answer(invocation);
} else {
Object ret = mockSettings.getDefaultAnswer().answer(invocation);

上述代码片段告诉我们,Mockito的录制与响应逻辑是,当缺少匹配的Stub invocation实例时,调用会被路由到InvocationImpl的callRealMethod方法上。打开其实现,马上一场骗局映入眼帘。

public Object callRealMethod() throws Throwable {
if (method.isAbstract()) {
new Reporter().cannotCallAbstractRealMethod();
}
return realMethod.invoke(mock, rawArguments);
}

在InvocationImpl.callRealMethod的实现中,根本就不是从MockSettingImpl中获取spiedInstance(即委托对象)进行invoke。而是诉诸于Mock代理对象的method.invoke。对于普通公开方法的调用,的确也没有问题,因为代理类都是继承或实现了委托目标类或接口的,因此对其他普通方法的调用最终还是会传递到委托类的方法调用上。但是,对于属性的访问,那就只能呵呵了。因为proxy对象的同名属性和target对象的同名属性,在内存中,可是两块独立的地址。这个在文章开始也讲过类似的问题,mockito在这里挖了一个坑。所以,mockito在spy场景的mock中,"补救式"的做了一个全量的属性copy...

事已至此,我能想到的办法有两个:

方案1. 在属性全量copy时,增加过滤设计。例如过滤CGLIB$CALLBAK(XXX)等名称的属性copy,这样就不会导致CALLBACK截杀。但我不想这么做:

直接的,这里也没有预留任何的属性过滤器回调,扩展难以优雅,而且即便有,以后还受动态代理的织入方式的影响。

根本的,这个实现多少有点hack的味道。文档告诉我spy对象没有录制匹配时,是调用被spy的委托对象,而实际上是在调用copy出来的代理实例!违反自然的设计,终将被时间证明其不合理性。

方案2. 去除这里的属性全量copy,让RealMethod的调用传递到对真实委托对象的调用上。是的,我觉得这种方式更合理。于是,重写MethodInterceptorFilter,覆盖其intercept方法。具体可以看下EduPowerMockMethodInterceptorFilter。(PS:示例这里我用了PowerMockito的

自定义Filter版本。因为底层mock框架往PowerMockito迁移的工作正做了一半,这一半也能配合正常工作,同时原理也一样,图个方便,我就拿这个讲了)

public Object superIntercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
if (objectMethodsGuru.isEqualsMethod(method)) {
return proxy == args[0];
} else if (objectMethodsGuru.isHashCodeMethod(method)) {
return hashCodeForMock(proxy);
} else if (acrossJVMSerializationFeature.isWriteReplace(method)) {
return acrossJVMSerializationFeature.writeReplace(proxy);
} MockitoMethodProxy mockitoMethodProxy = createMockitoMethodProxy(methodProxy);
new EduCGLIBHacker().setMockitoNamingPolicy(methodProxy); MockitoMethod mockitoMethod = createMockitoMethod(method); CleanTraceRealMethod realMethod = new CleanTraceRealMethod(mockitoMethodProxy);
Invocation invocation = new EduInvocationImpl(proxy, mockitoMethod, args, SequenceNumber.next(), realMethod,mockSettings.getSpiedInstance());
return getHandler().handle(invocation);
}

然后再看下扩展的EduInvocationImpl中的实现:

public EduInvocationImpl(Object mock, MockitoMethod mockitoMethod, Object[] args,
int sequenceNumber, RealMethod realMethod, Object target) {
super(mock, mockitoMethod, args, sequenceNumber, realMethod);
mockitoMethodHolder=mockitoMethod;
realMethodHolder=realMethod;
targetHolder=target;
}
public Object callRealMethod() throws Throwable {
if (mockitoMethodHolder.isAbstract()) {
new Reporter().cannotCallAbstractRealMethod();
}
return mockitoMethodHolder.getJavaMethod().invoke(targetHolder,getRawArguments());
//return realMethodHolder.invoke(targetHolder, getRawArguments());
}

在上述代码里面,我将具体的请求路由到对target的"反射版本"的method调用中。(PS:至于反射版本和字节码增强版本的method invokation的区别,等下次介绍PowerMockio版本实现时再一起带上)。这下,应该差不多了吧?不过经过这么几次折腾,我已经不再奢望一次成功。应该还会有什么幺蛾子吧,来吧!

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

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

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

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

    转载:https://sq.163yun.com/blog/article/169561874192850944 众所周知,mock对于单元测试,尤其是基于spring容器的单元测试,是非常重要的.它 ...

  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. MoQ(基于.net3.5,c#3.0的mock框架)简单介绍

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

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

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

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

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

随机推荐

  1. Elasticsearch创建索引和映射结构详解

    前言 这篇文章详细介绍了如何创建索引和某个类型的映射. 下文中[address]指代elasticsearch服务器访问地址(http://localhost:9200). 1       创建索引 ...

  2. hdu2476

    /* dp[l][r]表示将任意串的[l,r]刷成s2样子的最小代价 ans[i]表示将s1的前i位刷成s2的代价 按照区间dp的常用做法,dp[l][r]的状态由dp[l][k],dp[k+1][r ...

  3. ie7 下 float换行问题与vertical-align:middle; 失效问题

    声明:web小白的笔记,欢迎大神指点!联系QQ:1522025433. ie7 下 float换行问题 请直接看代码中和代码中的注释: <!doctype html> <html&g ...

  4. python 全栈开发,Day112(内容回顾,单例模式,路由系统,stark组件)

    一.内容回顾 类可否作为字典的key 初级 举例: class Foo(object): pass _registry = { Foo:123 } print(_registry) 执行输出: {&l ...

  5. SpringMVC异常处理注解@ExceptionHandler@ControllerAdvice@ResponseStatus

    参考: http://blog.csdn.net/w372426096/article/details/78429132 http://blog.csdn.net/w372426096/article ...

  6. Ubuntu 之 atom 安装以及 常用配置

    安装方式如下: 打开终端,使用以下命令安装: sudo add-apt-repository ppa:webupd8team/atom sudo apt-get update sudo apt-get ...

  7. 类 __new__方法实现单例

    继承了单例的类,子类也是单例模式

  8. 内置函数 filter zip map

    1. 基本内置函数: 2. enumerate :  枚举 把列表转化为有索引的字典: 3. eval 和 exec 4. 过滤函数  filter 5. map 函数批量修改: 6. 配对函数 zi ...

  9. ubuntu server 18.04的安装 以及配置网络还有ssh服务

    ubuntu server 18.04的安装 以及配置网络还有ssh服务   服务器是 dell T420 安装过程中规中矩,其中最关键的是分区部分,由于是服务器,如果磁盘比较大的话,一定要用 uef ...

  10. QT学习之第一个程序

    QT学习之第一个程序 目录 手动创建主窗口 居中显示 添加窗口图标 显示提示文本 Message Box的应用 手动连接信号与槽 手动创建主窗口 窗口类型 QMainWindow: 可以包含菜单栏.工 ...