一、介绍
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方法。