Spring Boot 单元测试自动执行测试脚本
起因
最近在给我司的微服务做架构升级,从 SpringBoot 1.5.x 升到 2.x。期间对配置做了一些改动,加上 SpringBoot 自身的配置变动,导致升级完以后,单元测试跑不过了。
我司的每一次代码合并,都需要保证通过单元测试。
究其原因,是因为数据不满足测试。数据都是一次性使用,每次进行单元测试时,数据脚本都会将上次新建的数据删除,再重新新建一份。
1.5.x
在1.5.x版本中, 数据脚本的执行,依赖于 Spring Boot 的自动化配置 DataSourceAutoConfiguration
和 DataSourceInitializer
。实际脚本的执行时依赖于 DataSourceInitializer
,不过DataSourceInitializer
的创建又依赖于 DataSourceAutoConfiguration
。所以需要一并分析这两个类。
当然,如果对于 Spring 容器管理 Bean 生命周期比较熟悉的同学,其实
DataSourceAutoConfiguration
是可以略过的。
DataSourceAutoConfiguration
DataSourceAutoConfiguration
实现了对 DataSource 的自动化配置(实际就是多种 @Conditional 注解的 Configuration 类),不过由于其支持的 DataSource 类型过于单一,所以一般不可能依赖于其自身的 DataSource 初始化。
我司的微服务,都是实现了自己的 DataSource 自动化配置,使用的是 DruidDataSource。
@Configuration
@ConditionalOnClass({
DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({
Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {
// 初始化时,注入 DataSourceProperties 和 上下文
@Bean
@ConditionalOnMissingBean
public DataSourceInitializer dataSourceInitializer(DataSourceProperties properties,
ApplicationContext applicationContext) {
return new DataSourceInitializer(properties, applicationContext);
}
/* 省略无关的代码*/
}
DataSourceAutoConfiguration
最主要的作用是声明了DataSourceInitializer
的实例化,并通过 @Import 依赖初始化了 Registrar
。
@EnableConfigurationProperties(DataSourceProperties.class)
同时初始化了 DataSourceProperties
类。
如果依赖数据源自动化配置的话,那么数据源的配置时依靠 DataSourceProperties
所存储的配置。
但是由于我司的数据源配置是自行配置初始化的,所以 DataSourceProperties
存在的意义就是自动执行测试脚本时的配置.
关键配置如下(1.5.x 与 2.x 相同):
# 定义自动化脚本的 DML 脚本
spring.datasource.schema=
# 定义自动化脚本的 DDL 脚本
spring.datasource.data=
# 由于已初始化数据源, 因此用不上的数据源配置
# 用于执行脚本时, 初始化数据源
spring.datasource.schemaUsername=
spring.datasource.schemaPassword=
spring.datasource.dataUsername=
spring.datasource.dataPassword=
对应的 DataSourceProperties 属性如下:
/**
* Schema (DDL) script resource references.
*/
private List<String> schema;
/**
* User of the database to execute DDL scripts (if different).
*/
private String schemaUsername;
/**
* Password of the database to execute DDL scripts (if different).
*/
private String schemaPassword;
/**
* Data (DML) script resource references.
*/
private List<String> data;
/**
* User of the database to execute DML scripts.
*/
private String dataUsername;
/**
* Password of the database to execute DML scripts.
*/
private String dataPassword;
那么 Registrar 的作用是什么?
其本身是为了注入一个 PostProcessor, 而 PostProcessor 的作用是为了在初始化 DataSource 之后立即初始化 DataSourceInitializer。
class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered {
private int order = Ordered.HIGHEST_PRECEDENCE;
@Override
// 最高优先级初始化、执行
public int getOrder() {
return this.order;
}
@Autowired
private BeanFactory beanFactory;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof DataSource) {
// force initialization of this bean as soon as we see a DataSource
// 当 DataSource 初始化之后, 立即强制初始化 DataSourceInitializer
this.beanFactory.getBean(DataSourceInitializer.class);
}
return bean;
}
/**
* {@link ImportBeanDefinitionRegistrar} to register the
* {@link DataSourceInitializerPostProcessor} without causing early bean instantiation
* issues.
*/
static class Registrar implements ImportBeanDefinitionRegistrar {
private static final String BEAN_NAME = "dataSourceInitializerPostProcessor";
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 为了注入 DataSourceInitializerPostProcessor 的 BeanDefinition
// 保证容器对其初始化
if (!registry.containsBeanDefinition(BEAN_NAME)) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// We don't need this one to be post processed otherwise it can cause a
// cascade of bean instantiation that we would rather avoid.
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
}
}
}
}
DataSourceInitializer
接下来看看主要的 DataSourceInitializer
。其负责脚本的自动执行,入口有两个:初始化后触发和事件触发。
class DataSourceInitializer implements ApplicationListener<DataSourceInitializedEvent> {
/* 所需的属性、数据源、上下文*/
private final DataSourceProperties properties;
private final ApplicationContext applicationContext;
private DataSource dataSource;
/* 标识是否已初始化脚本, 全局只执行一次 */
private boolean initialized = false;
@PostConstruct
public void init() {
// 配置 spring.datasource.initialize
// 默认为true, 也就是说 1.5.x 默认是自动执行初始化脚本的
if (!this.properties.isInitialize()) {
logger.debug("Initialization disabled (not running DDL scripts)");
return;
}
// 获取 DataSource
if (this.applicationContext.getBeanNamesForType(DataSource.class, false,
false).length > 0) {
this.dataSource = this.applicationContext.getBean(DataSource.class);
}
if (this.dataSource == null) {
logger.debug("No DataSource found so not initializing");
return;
}
// 执行 DML 脚本
runSchemaScripts();
}
@Override
public void onApplicationEvent(DataSourceInitializedEvent event) {
// 响应 DataSourceInitializedEvent(DataSource source) 事件
// 配置判断
if (!this.properties.isInitialize