找回密码
 立即注册
首页 业界区 业界 SpringBoot集成测试笔记:缩小测试范围、提高测试效率 ...

SpringBoot集成测试笔记:缩小测试范围、提高测试效率

赴忽 2025-9-24 13:51:18
背景

在 SpringBoot 中,除了基于 Mock 的单元测试,往往还需要执行几个模块组合的集成测试。一种简单的方法就是在测试类上加入 @SpringBootTest 注解,但是,如果不对该注解做一些配置,默认情况下该测试类会加载完整的 SpringBoot 环境,包括该程序中所有的 Bean。如果要初始化的 Bean 非常多,启动集成测试的时间就会很长,因此我们需要对 @SpringBootTest 注解进行一些配置,以减少环境加载的数量,提高程序运行效率。
项目架构

下面是一个简单的 SpringBoot 项目,类图如下:
1.png


  • ProjectController 依赖接口 ProjectListService 和 ProjectOperateService;
  • ProjectListService 的实现类依赖接口 ProjectConverter 和 ProjectMapper;
  • ProjectOperateService 的实现类依赖接口 ProjectBizCheckService、TechCheckService、ProjectConverver 和 ProjectMapper;
  • 接口 ProjectConverter 为 MapStruct映射接口;
  • 接口 ProjectMapper 为 Mybatis 数据访问接口(DAO)。
不带参数的 @SpringBootTest 测试类

从类图中可以看到,ProjectListService 的实现类依赖两个接口,分别是用于对象转换的 ProjectConverter 和数据访问接口 ProjectMapper。我们首先使用默认的配置,即不带参数的 @SpringBootTest 注解进行测试。测试类代码如下:
  1. /**
  2. * 直接采用 {@link SpringBootTest} 注解的集成测试,
  3. * 不带任何参数或配置
  4. *
  5. */
  6. @SpringBootTest
  7. @DisplayName("集成测试:不带任何参数或配置")
  8. class ProjectListServiceWithoutConfigsTest {
  9.     private final ApplicationContext applicationContext;
  10.     @Autowired
  11.     private ProjectListService projectListService;
  12.     public ProjectListServiceWithoutConfigsTest(ApplicationContext applicationContext) {
  13.         this.applicationContext = applicationContext;
  14.     }
  15.     @Test
  16.     @DisplayName("获取所有Bean名称和数量")
  17.     public void printAllBean() {
  18.         // 获取所有Bean名称
  19.         String[] beanNames = applicationContext.getBeanDefinitionNames();
  20.         Arrays.sort(beanNames);
  21.         System.out.println("========== Spring Beans Total (" + beanNames.length + ") =========");
  22.         for (String beanName : beanNames) {
  23.             System.out.println("name=" + beanName + ", 测试查询全部项目列表-应包含2个项目,且和数据库一致")
  24.     void testListAllProjects() {
  25.         List<ProjectListResponse> projectListResponses = projectListService.listProjects();
  26.         assertThat(projectListResponses).hasSize(2);
  27.         assertThat(projectListResponses.getFirst().getProjectId()).isEqualTo(1);
  28.         assertThat(projectListResponses.getFirst().getProjectName()).isEqualTo("测试项目1");
  29.         assertThat(projectListResponses.getFirst().getProjectStatus()).isEqualTo(ProjectStatus.READY.getDesc());
  30.         assertThat(projectListResponses.get(1).getProjectId()).isEqualTo(2);
  31.         assertThat(projectListResponses.get(1).getProjectName()).isEqualTo("测试项目2");
  32.         assertThat(projectListResponses.get(1).getProjectStatus()).isEqualTo(ProjectStatus.RUNNING.getDesc());
  33.     }
  34. }
复制代码
这里我们实现了一个方法 printAllBean(),通过获取应用上下文 ApplicationContext 对象中的所有被 Spring 加载的 Bean,检查本次测试加载的 Bean 数量。
运行测试,printAllBean() 方法的输出如下:
  1. ========== Spring Beans Total (289) =========
  2. name=/project, class=class cn.asuka.itd.project.controller.ProjectController
  3. name=accessorsProvider, class=class springfox.documentation.schema.property.bean.AccessorsProvider
  4. name=apiDescriptionLookup, class=class springfox.documentation.spring.web.scanners.ApiDescriptionLookup
  5. name=apiDescriptionReader, class=class springfox.documentation.spring.web.scanners.ApiDescriptionReader
  6. name=apiDocumentationScanner, class=class springfox.documentation.spring.web.scanners.ApiDocumentationScanner
  7. ......
  8. name=welcomePageNotAcceptableHandlerMapping, class=class org.springframework.boot.autoconfigure.web.servlet.WelcomePageNotAcceptableHandlerMapping
  9. name=xmlModelPlugin, class=class springfox.documentation.schema.plugins.XmlModelPlugin
  10. name=xmlPropertyPlugin, class=class springfox.documentation.schema.property.XmlPropertyPlugin
复制代码
可以看到总共加载了 289 个 Bean,数量很多,但大多数是我们在测试中不直接依赖的。
那么,我们应该如何让该测试只依赖我们需要的 Bean,或者尽可能减少依赖的 Bean 数量呢?
带参数的 @SpringBootTest 测试

首先,我们要知道的是基于 MapStruct 的 ProjectConverter 接口,在编译期会生成对应的实现类 ProjectConverterImpl,和 ProjectConverter 在同一个包下,因此我们实际上可以直接把该实现类加载进 Spring 上下文中。
但是,基于 Mybatis 的 ProjectMapper 并不会直接生成实现类,而是在运行期通过 MapperProxy 代理类去执行。此外我们使用的数据库连接池是 Druid,因此 ProjectMapper 也隐含了对 Druid 连接池的依赖。
因此,我们通过设置 @SpringBootTest 注解的 classes 参数,来指定本次测试中 Spring 上下文需要加载的类。
为了让测试代码能够调用 Druid 连接池,还需要建立一个 MybatisTestConfig 的配置类,人为地设置一个在测试环境下的 DataSource 对象,让我们的测试类依赖该数据源,而不是生产代码中的数据源。
MybatisTestConfig 配置类定义如下:
  1. /**
  2. * @author jwmao
  3. */
  4. @TestConfiguration
  5. @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class})
  6. @MapperScan(basePackages = {"cn.asuka.itd.project.dao"})
  7. public class MybatisTestConfig {
  8.     @Value("${spring.datasource.druid.url}")
  9.     private String url;
  10.     @Value("${spring.datasource.druid.username}")
  11.     private String username;
  12.     @Value("${spring.datasource.druid.password}")
  13.     private String password;
  14.     @Value("${spring.datasource.druid.driver-class-name}")
  15.     private String driverClassName;
  16.     @Bean
  17.     @ConfigurationProperties(prefix = "spring.datasource")
  18.     public DataSource dataSource() {
  19.         DruidDataSource dataSource = new DruidDataSource();
  20.         dataSource.setUrl(url);
  21.         dataSource.setUsername(username);
  22.         dataSource.setPassword(password);
  23.         dataSource.setDriverClassName(driverClassName);
  24.         return dataSource;
  25.     }
  26.     @Bean
  27.     public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  28.         SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
  29.         sessionFactory.setDataSource(dataSource);
  30.         sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
  31.                 .getResources("classpath:mapper/*.xml"));
  32.         return sessionFactory.getObject();
  33.     }
  34. }
复制代码
除了指定数据源 DataSource 对象,还需要指定 SqlSessionFactory 对象,因为 MapperProxy 类依赖它,如果不指定的话它不会自动注入。
下面来看一下 ProjectListServiceWithConfigsTest 的实现:
  1. /**
  2. * 指定测试依赖Bean的测试类
  3. */
  4. @SpringBootTest(classes = {
  5.         ProjectListServiceImpl.class,
  6.         MybatisTestConfig.class,
  7.         ProjectConverterImpl.class
  8. })
  9. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
  10. @PropertySource("classpath:application.properties")
  11. @DisplayName("集成测试:指定测试依赖Bean")
  12. class ProjectListServiceWithConfigsTest {
  13.     private final ApplicationContext applicationContext;
  14.     @Autowired
  15.     private ProjectListServiceImpl projectListServiceUnderTest;
  16.     public ProjectListServiceWithConfigsTest(ApplicationContext applicationContext) {
  17.         this.applicationContext = applicationContext;
  18.     }
  19.     @Test
  20.     @DisplayName("获取所有Bean名称和数量")
  21.     public void printAllBean() {
  22.         // 获取所有Bean名称
  23.         String[] beanNames = applicationContext.getBeanDefinitionNames();
  24.         Arrays.sort(beanNames);
  25.         System.out.println("========== Spring Beans Total (" + beanNames.length + ") =========");
  26.         for (String beanName : beanNames) {
  27.             System.out.println("name=" + beanName + ", 测试查询全部项目列表-应包含2个项目,且和数据库一致")
  28.     void testListAllProjects() {
  29.         List<ProjectListResponse> projectListResponses = projectListServiceUnderTest.listProjects();
  30.         assertThat(projectListResponses).hasSize(2);
  31.         assertThat(projectListResponses.getFirst().getProjectId()).isEqualTo(1);
  32.         assertThat(projectListResponses.getFirst().getProjectName()).isEqualTo("测试项目1");
  33.         assertThat(projectListResponses.getFirst().getProjectStatus()).isEqualTo(ProjectStatus.READY.getDesc());
  34.         assertThat(projectListResponses.get(1).getProjectId()).isEqualTo(2);
  35.         assertThat(projectListResponses.get(1).getProjectName()).isEqualTo("测试项目2");
  36.         assertThat(projectListResponses.get(1).getProjectStatus()).isEqualTo(ProjectStatus.RUNNING.getDesc());
  37.     }
  38. }
复制代码
在上述测试类中,两个测试方法 printAllBean() 和 testListAllProjects() 实现完全一致。在类上方的注解中,我们首先通过
  1. @SpringBootTest(classes = {
  2.         ProjectListServiceImpl.class,
  3.         MybatisTestConfig.class,
  4.         ProjectConverterImpl.class
  5. })
复制代码
分别指定我们需要测试的类 ProjectListServiceImpl、ProjectConverter 接口的实现类 ProjectConverterImpl 以及 Mybatis 配置类 MybatisTestConfig。然后通过 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 让测试类不加载默认的数据源,而是加载我们在 MybatisTestConfig 中配置的数据源;并通过 @PropertySource("classpath:application.properties") 来指定我们使用的测试配置文件。
运行测试类,可以看到 testListAllProjects() 同样可以测试通过,且 printAllBean() 的结果如下:
  1. ========== Spring Beans Total (27) =========
  2. name=cn.asuka.itd.testconfig.MybatisConfig#MapperScannerRegistrar#0, class=class org.mybatis.spring.mapper.MapperScannerConfigurer
  3. name=dataSource, class=class com.alibaba.druid.pool.DruidDataSource
  4. name=hikariPoolDataSourceMetadataProvider, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration$$Lambda/0x00000205685d79a0
  5. name=mybatisConfig, class=class cn.asuka.itd.testconfig.MybatisConfig$$EnhancerBySpringCGLIB$$7108b66c
  6. name=org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory, class=class org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory
  7. name=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  8. name=org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration
  9. name=org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration
  10. name=org.springframework.boot.context.internalConfigurationPropertiesBinder, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBinder
  11. name=org.springframework.boot.context.internalConfigurationPropertiesBinderFactory, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBinder$Factory
  12. name=org.springframework.boot.context.properties.BoundConfigurationProperties, class=class org.springframework.boot.context.properties.BoundConfigurationProperties
  13. name=org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
  14. name=org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter, class=class org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter$$Lambda/0x00000205685d7bb8
  15. name=org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, class=class org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration
  16. name=org.springframework.boot.test.context.ImportsContextCustomizer$ImportsCleanupPostProcessor, class=class org.springframework.boot.test.context.ImportsContextCustomizer$ImportsCleanupPostProcessor
  17. name=org.springframework.boot.test.mock.mockito.MockitoPostProcessor, class=class org.springframework.boot.test.mock.mockito.MockitoPostProcessor
  18. name=org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor, class=class org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor
  19. name=org.springframework.context.annotation.internalAutowiredAnnotationProcessor, class=class org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
  20. name=org.springframework.context.annotation.internalCommonAnnotationProcessor, class=class org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
  21. name=org.springframework.context.annotation.internalConfigurationAnnotationProcessor, class=class org.springframework.context.annotation.ConfigurationClassPostProcessor
  22. name=org.springframework.context.event.internalEventListenerFactory, class=class org.springframework.context.event.DefaultEventListenerFactory
  23. name=org.springframework.context.event.internalEventListenerProcessor, class=class org.springframework.context.event.EventListenerMethodProcessor
  24. name=projectConverterImpl, class=class cn.asuka.itd.converter.ProjectConverterImpl
  25. name=projectListServiceImpl, class=class cn.asuka.itd.project.service.impl.ProjectListServiceImpl
  26. name=projectMapper, class=class jdk.proxy2.$Proxy84
  27. name=spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties, class=class org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
  28. name=sqlSessionFactory, class=class org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
复制代码
可以看到只加载了 27 个 Bean,大大减少了 Bean 的加载数量,对测试运行速度提升也有帮助。
总结

如果需要指定需要测试的 Bean 及其依赖,而不是加载完整的上下文环境,可以在 @SpringBootTest 注解的 classes 参数中配置需要测试及依赖的类或对象。如果遇到不是项目中自己写的或者可以自动生成的实现类,可以通过配置 @TestConfiguration 的方式,在测试配置中注册相关的 Bean。最终做到缩小测试范围,提高测试运行效率。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册