【无标题】

优化基于@SpringBootTest 的测试案例

  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。简直不可接受:

默认装配的Bean数量

分析 @SpringBootTest 注解的属性,可以通过 classes 指定 Spring Boot 启动时加载配置类或者 Bean 的定义类。
此处需要说明的是 classes 属性必须引入了 Spring 支持的基于 Java Config 支持的配置方式的类,比如标注了 @Configuration 、@SpringBootConfiguration 或 @Component注解的配置类或者普通的定义 Bean 的类,才能够阻止默认查找标注了 @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);
}

}
运行结果如下,总共只导入了 9 个 Bean:
自定义引导类

如果需要对某个目录下的 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 中所述。

  1. 对 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 加载的数量,来对测试的执行进行提速。

来源:优化基于@SpringBootTest 的测试案例,让你的测试飞起来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值