1. 问题背景
单元测试是保证代码业务质量最有效的手段。Spring Boot 为开发者提供了便利的切片和集成测试工具和注解。此处为什么不说单元测试,作者认为单元测试的前提是不启动 ApplicationContext 容器。那采用 @SpringBootTest 进行测试案例的编写,如何进行提速呢?
2. 原理分析
此处默认大家集成 Junit5 进行测试案例的编写。@SpringBootTest 默认会搜索源码路径下标注 @SpringBootConfiguration 的类。并通过该类作为启动类。因此,在默认的情况下 @SpringBootTest 会查找到应用程序的启动类,也就是标注了 @SpringBootApplication 的类。比如下边的 SpringBootTestApplication 类。
@SpringBootApplication
public class SpringBootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTestApplication.class, args);
}
}
通过上述默认的启动方式,@SpringBootTest 执行如下启动逻辑:
- @EnableAutoConfiguration 开启自动装配,装备类路径下所有的*AutoConfiguration 逻辑
- @ComponentScan 开启工程源码路径下的所有 Bean 的装配
3. 优化提速
基于上述分析,要优化 @SpringBootTest 的执行速度,可以从如下两方面入手:
3.1 禁止自动装配的执行
以一个简单的 Spring Boot 集成了 Spring MVC 和 MyBatis 的项目分析,启动默认的 @SpringBootTest 案例,发现竟然总共装配了近 270 个 Bean。简直不可接受:
分析 @SpringBootTest 注解的属性,可以通过 classes
指定 Spring Boot 启动时加载配置类或者 Bean 的定义类。
此处需要说明的是 classes 属性必须引入标注了 @Configuration
或者 @SpringBootConfiguration
注解的配置类,才能够阻止默认查找标注了 @SpringBootConfiguration
的配置类。如下代码所示:
// 或者标注 @SpringBootConfiguration
@Configuration
public class OuterConfig {
}
@SpringBootTest(classes = {OuterConfig.class})
class SpringBootTestApplicationTests {
@Autowired
private ApplicationContext applicationContext;
@Test
void contextLoads() {
System.out.println(applicationContext.getBeanDefinitionCount());
Stream.of(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
}
如果需要对某个目录下的 Bean 进行扫描装配,只需要在 OuterConfig
上增加 @ComponentScan
并指定扫描路径即可,如:
// 或者标注 @SpringBootConfiguration
@Configuration
@ComponentScan("cn.cincout.spring.boot.springboottest.application.calculate")
public class OuterConfig {
}
如果仅仅需要测试自己编写的某个 Service 类,可进行如下编写:
@Service
@Slf4j
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int a, int b) {
log.info("calculate a + b");
return new Calculator().add(a, b);
}
}
/**
* 如果指定 @SpringBootTest(classes=“”) 就不会查找默认的 @SpringBootConfiguration 的类
* 在这种情况下内部静态类也不会被默认加载,必须显式的引入
* 但是会执行 Spring Boot 提供的 properties 文件的读取,但是不会
* 实现 @@ConfigurationProperties 注解的逻辑
* @sine 1.8
*/
@SpringBootTest(classes = {CalculateServiceImpl.class, CalculateServiceImplWithSpringBootTestITTest.InnerConfig.class})
class CalculateServiceImplWithSpringBootTestITTest {
@Autowired
ApplicationContext applicationContext;
@Autowired
CalculateService calculateService;
/**
* 不会被默认加载
*/
@Configuration
protected static class InnerConfig {
}
@Test
void add() {
System.out.println(applicationContext.getBeanDefinitionCount());
int result = calculateService.add(1, 2);
Assertions.assertEquals(3, result);
}
}
3.2 设置 WebEnvironment.NONE
如果不是测试 WebMvc,强列建议关闭 WebEnvironment.NONE 能够少实例化几十个 Bean。
/**.
* just init 9 bean without auto config and component scan
* 如果 @SpringBootTest 在当前路径下可以找到 @SpringBootConfiguration 注解的类,可以使用该类作为默认配置
* 如果提供了 @Configuration 作为测试类的内部静态配置类,@SpringBootTest 将使用该类作为默认配置,此时自动装配不再生效
* 如果主动配置 @SpringBootTest(classes={}) ,@SpringBootTest 也不会寻找默认的注解了@SpringBootConfiguration 的类
*
* @TestConfiguration 只能用来添加测试Bean 或者 对现有的 Bean 进行覆盖,如果是覆盖需要添加
* spring.main.allow-bean-definition-overriding=true 配置参数
* @TestConfiguration 不会阻止 @SpringBootTest 寻找 @SpringBootConfiguration 注解的类
*
* SpringBootContextLoader 会将当前被测试的类 设置为 setMainApplicationClass,将内部静态配置类和@SpringBootTest(classes={})
* 引入的配置类作为 addPrimarySources
* @sine 1.8
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class SpringBootWebProjectTestWithNoneWebEnvironmentConfigTest {
@Autowired
ApplicationContext applicationContext;
@Test
void contextLoad() {
System.out.println(applicationContext.getBeanDefinitionCount());
Stream.of(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
}
/**
* 如果使用内部静态类 @TestConfiguration,依然回去查找 @SpringBootConfiguration
*/
@TestConfiguration
protected static class InnerConfig {
}
}
3.3 自定义 @ComponentScan
通过自定义指定 @ComponentScan 扫描的路径,可以有效的减少 ApplicationContext 实例化的 Bean 的数量。如 3.1 中所述。
4. 对 Spring Boot 属性配置文件的测试
关闭了自动装配, @ConfigurationProperties
标注的类的属性注入将会失败,但是 Spring Boot 已经读取了 properties 文件中的属性(系统环境变量、Java 环境变量)存储到Environment。该功能由ConfigDataEnvironmentPostProcessor
实现读取(2.4.0 版本后),该版本之前由 ConfigFileApplicationListener
读取。
那么如何开启 @ConfigurationProperties 功能呢?只需要在自定义配置类上加上 @EnableConfigurationProperties
,如下:
@Data
@ConfigurationProperties(prefix = "pool")
@Component
public class PoolConfigProperties {
private int maxTotal;
private int minIdle;
private String threadCount;
}
/**
* @SpringBootTest(classes="") 通过该方法配置时,基于@ConfigurationProperties
* 的属性注入失败,需要增加配置类 标注 @EnableConfigurationProperties
* @ComponentScan 会扫描源码和测试代码中相同路径下的 Bean
* @sine 1.8
*/
@SpringBootTest(classes = {PoolConfigPropertiesWithSpringBootTest.InnerConfig.class, PoolConfigProperties.class})
class PoolConfigPropertiesWithSpringBootTest {
@Autowired
private PoolConfigProperties poolConfigProperties;
@Autowired
ApplicationContext applicationContext;
@Configuration
//@ComponentScan("cn.cincout.spring.boot.springboottest.application.properties")
@EnableConfigurationProperties
protected static class InnerConfig {
}
@Test
void getMaxTotal() {
System.out.println(applicationContext.getBeanDefinitionCount());
Stream.of(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
int maxTotal = poolConfigProperties.getMaxTotal();
Assertions.assertEquals(10, maxTotal);
}
}
除上述之外,可以通过 spring-test 提供的 @ContextConfiguration
实现 @ConfigurationProperties
。如下:
/**
* 可以 @TestPropertySource 和 @EnableConfigurationProperties 实现 @ConfigurationProperties(prefix = "pool")注解的功能
* @sine 1.8
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {PoolConfigPropertiesTest.InnerConfig.class, PoolConfigProperties.class})
@TestPropertySource(locations = {"classpath:application.properties"})
class PoolConfigPropertiesTest {
@Autowired
private PoolConfigProperties poolConfigProperties;
@TestConfiguration
@EnableConfigurationProperties
protected static class InnerConfig {
}
@Test
void getMaxTotal() {
int maxTotal = poolConfigProperties.getMaxTotal();
Assertions.assertEquals(10, maxTotal);
}
}
除上述外,可以采用 @SpringJUnitConfig
替代 @ExtendWith(SpringExtension.class)
和 @ContextConfiguration
。如下:
@SpringJUnitConfig(classes = {CalculateServiceImpl.class})
class CalculateServiceImplWithSpringJunitConfigTest {
@Autowired
CalculateService calculateService;
@Autowired
ApplicationContext applicationContext;
@Test
void add() {
System.out.println(applicationContext.getBeanDefinitionCount());
Stream.of(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
int result = calculateService.add(1, 2);
Assertions.assertEquals(3, result);
}
}
5. 装配指定的部分自动装配*AutoConfiguration
分析 Spring Boot 或 Mybatis 提供的切片测试注解可知,通过组合下列注解,可以实现指定需要自动装配的配置类进行加载装配,比如 @MybatisTest
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(MybatisTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(MybatisTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureMybatis
@AutoConfigureTestDatabase
@ImportAutoConfiguration
public @interface MybatisTest {
}
通过分析 @MybatisTest
,我们想测试 @Mapper 时,可以通过如下方式实现部分自动装配(当然也可以实用 @MybatisTest 注解):
@SpringBootTest(classes = {CustomerMapperWithAutoMybatisTest.InnerMybatisConfig.class},
webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
class CustomerMapperWithAutoMybatisTest {
@Autowired
private CustomerMapper customerMapper;
@Autowired
private ApplicationContext applicationContext;
// 可以用 @Configuration 替代
@SpringBootConfiguration
// DataSourceAutoConfiguration 装配时需要该属性 Bean
@EnableConfigurationProperties(value = {DataSourceProperties.class})
// 导入需要自动装配的配置类
@ImportAutoConfiguration(classes = {
DataSourceAutoConfiguration.class,
MybatisAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
// 关闭默认的自动装配
@OverrideAutoConfiguration(enabled = false)
// 扫描 @Mapper
@MapperScan("cn.cincout.spring.boot.springboottest.inf.persistence.mapper")
protected static class InnerMybatisConfig {
}
@Test
@Sql(scripts = {"classpath:/sql/insertCustomer.sql"})
@Rollback(value = true)
void selectById() {
System.out.println(applicationContext.getBeanDefinitionCount());
Stream.of(applicationContext.getBeanDefinitionNames()).forEach(System.out::println);
CustomerEntity entity = customerMapper.selectById(2);
Assertions.assertNotNull(entity);
}
}
6. 总结
通过上述的案例及实例,想要优化 @SpringBootTest 执行速度,最核心的就是减少加载的 Bean。包括自动装配 *AutoConfiguration
和 @ComponentScan
。
实际在编写测试的时候,我们可以从以下三类测试类型入手:
- 单元测试 不启动 ApplicationContext,通过 Mock 的方式解除外部依赖,速度是最快的;
- 切片测试 代码按分层组织的形式,针对某层进行测试,比如 DAO、DOMAIN、SERVICE、CONTROLLER 等,详细的后续会继续写文章进行介绍;
- 集成测试 启动 ApplicationContext,对多个组件进行集成测试,但是我们依然可以通本文介绍的方式减少 Bean 加载的数量,来对测试的执行进行提速。