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源码赏析


spring boot 测试研究_第1张图片

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


spring boot 测试研究_第2张图片


这里就是对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 测试研究_第3张图片

八、总结

拨开云雾见青天,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方法。

你可能感兴趣的:(spring boot 测试研究)