spring boot 测试研究

一、介绍

      Spring Boot提供很多有用的工具类和注解用于帮助你测试应用,主要分两个模块:spring-boot-test包含核心组件,spring-boot-test-autoconfigure为测试提供自动配置。

大多数开发者只需要引用spring-boot-starter-test ‘Starter’,它既提供Spring Boot测试模块,也提供JUnit,AssertJ,Hamcrest和很多有用的依赖。

      有时候需要在运行测试用例时mock一些组件,例如,你可能需要一些远程服务的门面,但在开发期间不可用。Mocking在模拟真实环境很难复现的失败情况时非常有用。

Spring Boot提供一个@MockBean注解,可用于为ApplicationContext中的bean定义一个Mockito mock,你可以使用该注解添加新beans,或替换已存在的bean定义。该注解可直接用于测试类,也可用于测试类的字段,或用于@Configuration注解的类和字段。当用于字段时,创建mock的实例也会被注入。Mock beans每次调用完测试方法后会自动重置。

   一般的mock流程为:

        1. 建立mock;

        2. 将mock和待测试的对象连接起来;

        3. 在mock上设置预期的返回值;

        4. 开启replay模式,准备记录实际发生的调用;

        5. 进行测试;

        6. 验证测试结果,调用顺序是否正确,返回值是否符合期望;

二、spring boot test版本变化

1. 没有Spring的测试

SomeService service = mock(SomeService.class);   

2. Spring Boot 1.3的测试

@SpringApplicationConfiguration:

@RunWith(SpringJUnit4ClassRunner.class)

@SpringApplicationConfiguration(MyApp.class)

public class MyTest {  

  // ...

}

3.Spring Boot 1.4简单化测试

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)

public class MyTest {  

  // ...   

 }

三、Mocking和Spying利器

Spring boot使用@MockBean和@SpyBean来定义Mockito的mock和spy。

@RunWith(SpringRunner.class)

@SpringBootTestpublic 

class MyTests {

    @MockBean   

     private RemoteService remoteService;

    @Autowired    

    private Reverser reverser;

    @Test    

     public void exampleTest() {

        // RemoteService has been injected into the reverser bean        

        given(this.remoteService.someCall()).willReturn("mock");

        String reverse = reverser.reverseSomeCall();

        assertThat(reverse).isEqualTo("kcom");

    }

}

四、MockBean使用研究

mockBean主要是对两个不需要验证的类进行mock,只需要在需要mock的类上加上@MockBean注解,可以自动将该类注入到带有@Autowired注解的实例中。

当Bean存在这种依赖关系当时候:LooImpl -> FooImpl -> Bar,我们应该怎么测试呢?

按照Mock测试的原则,这个时候我们应该mock一个Foo对象,把这个注入到LooImpl对象里,这里最直接的mock。

不过如果你不想mock Foo而是想mock Bar的时候,其实做法和前面也差不多,Spring会自动将mock Bar注入到FooImpl中,然后将FooImpl注入到LooImpl中。俗称隔代注入。

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)

  Class Boot_1_Test{

  @MockBean  

    private Bar bar;

  @Autowired  

   private Loo loo;

  @Test  

   public void testCheckCodeDuplicate() throws Exception {

    when(bar.getAllCodes()).thenReturn(Collections.singleton("123"));

    assertEquals(foo.checkCodeDuplicate("123"), true);

  }

}

五、MockBean遇上dubbo

@Service

public class MyApi {

    @Reference

    private RemoteApi remoteApi;

    public String hold() {

        return remoteApi.hold();

    }

}

@SpringBootApplication

public class ConsumerTest {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerTest.class, args);

    }

    @Bean

    public RemoteApi RemoteApi() {

        RemoteApi remoteApi = PowerMockito.mock(RemoteApi.class);

        PowerMockito.when(remoteApi.hold())

                .thenAnswer(t -> "我是Mock的API。");

        return remoteApi;

    }

}

@RunWith(SpringRunner.class)

@SpringBootTest(classes = ConsumerTest.class)

public class MyApiTest {

    @Autowired

    private ApplicationContext applicationContext;

    @Before

    public void before() {

        MyApi myApi = applicationContext.getBean(MyApi.class);

        RemoteApi fromMyApi = myApi.getRemoteApi();

        RemoteApi fromSpring = applicationContext.getBean(RemoteApi.class);

        System.out.println("MyApi中注入的RemoteApi是:" + fromMyApi);

        System.out.println("Spring容器中注入的RemoteApi是:" + fromSpring);

    }

    @Autowired

    public MyApi myApi;

    @Test

    public void hold() {

        Assert.assertEquals("我是Mock的API。", this.myApi.hold());

    }

}

MyApi中注入的RemoteApi是:com.alibaba.dubbo.common.bytecode.proxy0@541afb85

Spring容器中注入的RemoteApi是:remoteApi

结论分析如下:

Dubbo的Reference拿到的一个dubbo相关的代理;

Reference生成的代理和spring生成的代理,不能进行直接等价。

解决

原因我们知道了,要如何解决呢?答案很简单——如果我们在执行单元测试之前,将StoreApi中注入的RemoteApi换成Spring容器中的实例(即我们Mock的那个对象),那么问题就可以得到就解决。

@RunWith(SpringRunner.class)

@ActiveProfiles("qa")

@TestExecutionListeners({MockListener.class,})

@ContextConfiguration(locations = {"classpath*:config-spring-test.xml"})

public abstract class BaseBootTest {

protected Loggerlogger = LoggerFactory.getLogger(getClass());

}

public class MockListener extends DependencyInjectionTestExecutionListener {

@Override

protected void injectDependencies(final TestContext testContext)throws Exception {

super.injectDependencies(testContext);

    init(testContext);

}

}

private void init(final TestContext testContext)throws Exception {

   Object bean = testContext.getTestInstance();

   Field[] fields = bean.getClass().getDeclaredFields();

   接下来就可以通过反射来手动mock并注入到目标对象中。

}


六、原因分析

以上已经提供了解决方案。那么,Reference究竟干了哪些事情呢?我们不妨分析一下。

Reference被哪些地方使用,可找到以下代码:

com.alibaba.dubbo.config.spring.AnnotationBean#postProcessBeforeInitialization 。以该代码是我们定位问题的入口,由此,我们可以定位到以下两个方法:

com.alibaba.dubbo.config.ReferenceConfig#init

com.alibaba.dubbo.config.ReferenceConfig#createProxy

其中,

createProxy方法用于创建代理对象;

init方法用来判断是否已经初始化,如果没有初始化,就会调用createProxy创建代理对象。

了解Dubbo如何创建对象后,我们来看看Dubbo是如何将代理对象设置到MyApi的,如下图。

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

    if(!this.isMatchPackage(bean)) {

        return bean;

    } else {

        Class clazz = bean.getClass();

        if(this.isProxyBean(bean)) {

            clazz = AopUtils.getTargetClass(bean);

        }

        Method[] methods = clazz.getMethods();

        Method[] fields = methods;

        int var6 = methods.length;

        int var7;

        Reference e;

        Object value;

        for(var7 = 0; var7 < var6; ++var7) {

            Method method = fields[var7];

            String field = method.getName();

            if(field.length() > 3 && field.startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())) {

                try {

                    e = (Reference)method.getAnnotation(Reference.class);

                    if(e != null) {

                        value = this.refer(e, method.getParameterTypes()[0]);

                        if(value != null) {

                            method.invoke(bean, new Object[]{value});

                        }

                    }

                } catch (Exception var12) {

                    throw new BeanInitializationException("Failed to init remote service reference at method " + field + " in class " + bean.getClass().getName(), var12);

                }

            }

        }

        Field[] var14 = clazz.getDeclaredFields();

        Field[] var15 = var14;

        var7 = var14.length;

        for(int var16 = 0; var16 < var7; ++var16) {

            Field var17 = var15[var16];

            try {

                if(!var17.isAccessible()) {

                    var17.setAccessible(true);

                }

                e = (Reference)var17.getAnnotation(Reference.class);

                if(e != null) {

                    value = this.refer(e, var17.getType());

                    if(value != null) {

                        var17.set(bean, value);  // 这里注入的是dubbo的代理类

                    }

                }

            } catch (Exception var13) {

                throw new BeanInitializationException("Failed to init remote service reference at filed " + var17.getName() + " in class " + bean.getClass().getName(), var13);

            }

        }

        return bean;

    }

}

七、mockBean源码赏析


public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanClassLoaderAware, BeanFactoryAware, BeanFactoryPostProcessor, Ordered

继承InstantiationAwareBeanPostProcessorAdapter,说明是扩展spring功能,即bean加载完成后的mock处理.

实现类BeanFactoryPostProcessor 中的接口  postProcessBeanFactory来实现主要逻辑。

beanFactory.registerSingleton(MockitoBeans.class.getName(), this.mockitoBeans);  将所有的mockBean对象注册为单例形式

采用spring boot独有的解析器来读取bean定义的信息

DefinitionsParser parser = new DefinitionsParser(this.definitions);

Iterator definitions = this.getConfigurationClasses(beanFactory).iterator();

while(definitions.hasNext()) {

    Class configurationClass = (Class)definitions.next();

    parser.parse(configurationClass);

}

这里是重新定义解析器,解析出带有MockBean后者SpyBean字段,包装成新的AnnotatedElement



这里就是对MockBean 和Spy 注解的类型,进行mock注入

private void register(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry, Definition definition, Field field) {

    if(definition instanceof MockDefinition) {

        this.registerMock(beanFactory, registry, (MockDefinition)definition, field);

    } else if(definition instanceof SpyDefinition) {

        this.registerSpy(beanFactory, registry, (SpyDefinition)definition, field);

    }

}

最后一步:

创建mock对象: Object mock = this.createMock(definition, beanName);

beanFactory.registerSingleton(transformedBeanName, mock);    注册后为单例

this.mockitoBeans.add(mock);    缓存mock信息

this.beanNameRegistry.put(definition, beanName);    // 将mock信息进行注册

if(field != null) {

    this.fieldRegistry.put(field, new MockitoPostProcessor.RegisteredField(definition, beanName));  // 这里是关键,将mock对象,注入到目标对象的中

}

八、总结

拨开云雾见青天,spring boot test的mock机制其实就是将一个mock对象注入到另一个对象的字段中。而这种注入主要是依赖反射代理来实现。

spring boot默认会启动容器,来进行bean的装配,这就需要抓住进行mock注入的时机,即bean的装配完成后,将mock对象替换目标对象。

spring是一个伟大的框架,其伟大不在于本身能提供多么强大的功能,而在于,是一种微内核+插件的原理形态,可以供广大开发者开发各种强大的插件功能,扩展性非常好。总结起来,有如下形态:

1. 使用BeanPostProcessor来定制bean

  BeanPostProcess接口有两个方法,都可以见名知意:

(1)postProcessBeforeInitialization:在初始化Bean之前

(2)postProcessAfterInitialization:在初始化Bean之后

2. 使用BeanFactoryPostProcessor来自定义配置数据

Spring允许在Bean创建之前,读取Bean的元属性,并根据自己的需求对元属性进行改变,比如将Bean的scope从singleton改变为prototype,最典型的应当是PropertyPlaceholderConfigurer,替换xml文件中的占位符,替换为properties文件中相应的key对应的value。

3. 使用一个工厂bean来定制实例化逻辑,魔法FactoryBean

传统的Spring容器加载一个Bean的整个过程,都是由Spring控制的,换句话说,开发者除了设置Bean相关属性之外,是没有太多的自主权的。FactoryBean改变了这一点,开发者可以个性化地定制自己想要实例化出来的Bean,方法就是实现FactoryBean接口。

4.InitialingBean和DisposableBean

InitialingBean是一个接口,提供了一个唯一的方法afterPropertiesSet()。

DisposableBean也是一个接口,提供了一个唯一的方法destory()。

这两个接口是一组的,功能类似,因此放在一起:前者顾名思义在Bean属性都设置完毕后调用afterPropertiesSet()方法做一些初始化的工作,后者在Bean生命周期结束前调用destory()方法做一些收尾工作。

5.InstantiationAwareBeanPostProcessor

   InstantiationAwareBeanPostProcessor作用的是Bean实例化前后,即:

(1)Bean构造出来之前调用postProcessBeforeInstantiation()方法

(2)Bean构造出来之后调用postProcessAfterInstantiation()方法

6.各种Aware: BeanNameAware、ApplicationContextAware和BeanFactoryAware

"Aware"的意思是"感知到的",那么这三个接口的意思也不难理解:

(1)实现BeanNameAware接口的Bean,在Bean加载的过程中可以获取到该Bean的id

(2)实现ApplicationContextAware接口的Bean,在Bean加载的过程中可以获取到Spring的ApplicationContext,这个尤其重要,ApplicationContext是Spring应用上下文,从ApplicationContext中可以获取包括任意的Bean在内的大量Spring容器内容和信息

(3)实现BeanFactoryAware接口的Bean,在Bean加载的过程中可以获取到加载该Bean的BeanFactory

7. 各种事件定义和事件监听

 (1)通过扩展ApplicationEvent,创建一个事件类CustomEvent。这个类必须定义一个默认的构造函数,它应该从ApplicationEvent类中继承的构造函数。

(2)一旦定义事件类,你可以从任何类中发布它,假定EventClassPublisher实现了ApplicationEventPublisherAware。你还需要在XML配置文件中声明这个类作为一个bean,之所以容器可以识别bean作为事件发布者,是因为它实现了ApplicationEventPublisherAware接口。

(3)发布的事件可以在一个类中被处理,假定EventClassHandler实现了ApplicationListener接口,而且实现了自定义事件的onApplicationEvent方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容