SpringBoot集成測試筆記:縮小測試范圍、提高測試效率
背景
在 SpringBoot 中,除了基于 Mock 的單元測試,往往還需要執行幾個模塊組合的集成測試。一種簡單的方法就是在測試類上加入 @SpringBootTest 注解,但是,如果不對該注解做一些配置,默認情況下該測試類會加載完整的 SpringBoot 環境,包括該程序中所有的 Bean。如果要初始化的 Bean 非常多,啟動集成測試的時間就會很長,因此我們需要對 @SpringBootTest 注解進行一些配置,以減少環境加載的數量,提高程序運行效率。
項目架構
下面是一個簡單的 SpringBoot 項目,類圖如下:

ProjectController依賴接口ProjectListService和ProjectOperateService;ProjectListService的實現類依賴接口ProjectConverter和ProjectMapper;ProjectOperateService的實現類依賴接口ProjectBizCheckService、TechCheckService、ProjectConverver和ProjectMapper;- 接口
ProjectConverter為MapStruct映射接口; - 接口
ProjectMapper為Mybatis數據訪問接口(DAO)。
不帶參數的 @SpringBootTest 測試類
從類圖中可以看到,ProjectListService 的實現類依賴兩個接口,分別是用于對象轉換的 ProjectConverter 和數據訪問接口 ProjectMapper。我們首先使用默認的配置,即不帶參數的 @SpringBootTest 注解進行測試。測試類代碼如下:
/**
* 直接采用 {@link SpringBootTest} 注解的集成測試,
* 不帶任何參數或配置
*
*/
@SpringBootTest
@DisplayName("集成測試:不帶任何參數或配置")
class ProjectListServiceWithoutConfigsTest {
private final ApplicationContext applicationContext;
@Autowired
private ProjectListService projectListService;
public ProjectListServiceWithoutConfigsTest(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Test
@DisplayName("獲取所有Bean名稱和數量")
public void printAllBean() {
// 獲取所有Bean名稱
String[] beanNames = applicationContext.getBeanDefinitionNames();
Arrays.sort(beanNames);
System.out.println("========== Spring Beans Total (" + beanNames.length + ") =========");
for (String beanName : beanNames) {
System.out.println("name=" + beanName + ", class=" + applicationContext.getBean(beanName).getClass());
}
}
@Test
@DisplayName("測試查詢全部項目列表-應包含2個項目,且和數據庫一致")
void testListAllProjects() {
List<ProjectListResponse> projectListResponses = projectListService.listProjects();
assertThat(projectListResponses).hasSize(2);
assertThat(projectListResponses.getFirst().getProjectId()).isEqualTo(1);
assertThat(projectListResponses.getFirst().getProjectName()).isEqualTo("測試項目1");
assertThat(projectListResponses.getFirst().getProjectStatus()).isEqualTo(ProjectStatus.READY.getDesc());
assertThat(projectListResponses.get(1).getProjectId()).isEqualTo(2);
assertThat(projectListResponses.get(1).getProjectName()).isEqualTo("測試項目2");
assertThat(projectListResponses.get(1).getProjectStatus()).isEqualTo(ProjectStatus.RUNNING.getDesc());
}
}
這里我們實現了一個方法 printAllBean(),通過獲取應用上下文 ApplicationContext 對象中的所有被 Spring 加載的 Bean,檢查本次測試加載的 Bean 數量。
運行測試,printAllBean() 方法的輸出如下:
========== Spring Beans Total (289) =========
name=/project, class=class cn.asuka.itd.project.controller.ProjectController
name=accessorsProvider, class=class springfox.documentation.schema.property.bean.AccessorsProvider
name=apiDescriptionLookup, class=class springfox.documentation.spring.web.scanners.ApiDescriptionLookup
name=apiDescriptionReader, class=class springfox.documentation.spring.web.scanners.ApiDescriptionReader
name=apiDocumentationScanner, class=class springfox.documentation.spring.web.scanners.ApiDocumentationScanner
......
name=welcomePageNotAcceptableHandlerMapping, class=class org.springframework.boot.autoconfigure.web.servlet.WelcomePageNotAcceptableHandlerMapping
name=xmlModelPlugin, class=class springfox.documentation.schema.plugins.XmlModelPlugin
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 配置類定義如下:
/**
* @author jwmao
*/
@TestConfiguration
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class})
@MapperScan(basePackages = {"cn.asuka.itd.project.dao"})
public class MybatisTestConfig {
@Value("${spring.datasource.druid.url}")
private String url;
@Value("${spring.datasource.druid.username}")
private String username;
@Value("${spring.datasource.druid.password}")
private String password;
@Value("${spring.datasource.druid.driver-class-name}")
private String driverClassName;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return sessionFactory.getObject();
}
}
除了指定數據源 DataSource 對象,還需要指定 SqlSessionFactory 對象,因為 MapperProxy 類依賴它,如果不指定的話它不會自動注入。
下面來看一下 ProjectListServiceWithConfigsTest 的實現:
/**
* 指定測試依賴Bean的測試類
*/
@SpringBootTest(classes = {
ProjectListServiceImpl.class,
MybatisTestConfig.class,
ProjectConverterImpl.class
})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@PropertySource("classpath:application.properties")
@DisplayName("集成測試:指定測試依賴Bean")
class ProjectListServiceWithConfigsTest {
private final ApplicationContext applicationContext;
@Autowired
private ProjectListServiceImpl projectListServiceUnderTest;
public ProjectListServiceWithConfigsTest(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Test
@DisplayName("獲取所有Bean名稱和數量")
public void printAllBean() {
// 獲取所有Bean名稱
String[] beanNames = applicationContext.getBeanDefinitionNames();
Arrays.sort(beanNames);
System.out.println("========== Spring Beans Total (" + beanNames.length + ") =========");
for (String beanName : beanNames) {
System.out.println("name=" + beanName + ", class=" + applicationContext.getBean(beanName).getClass());
}
}
@Test
@DisplayName("測試查詢全部項目列表-應包含2個項目,且和數據庫一致")
void testListAllProjects() {
List<ProjectListResponse> projectListResponses = projectListServiceUnderTest.listProjects();
assertThat(projectListResponses).hasSize(2);
assertThat(projectListResponses.getFirst().getProjectId()).isEqualTo(1);
assertThat(projectListResponses.getFirst().getProjectName()).isEqualTo("測試項目1");
assertThat(projectListResponses.getFirst().getProjectStatus()).isEqualTo(ProjectStatus.READY.getDesc());
assertThat(projectListResponses.get(1).getProjectId()).isEqualTo(2);
assertThat(projectListResponses.get(1).getProjectName()).isEqualTo("測試項目2");
assertThat(projectListResponses.get(1).getProjectStatus()).isEqualTo(ProjectStatus.RUNNING.getDesc());
}
}
在上述測試類中,兩個測試方法 printAllBean() 和 testListAllProjects() 實現完全一致。在類上方的注解中,我們首先通過
@SpringBootTest(classes = {
ProjectListServiceImpl.class,
MybatisTestConfig.class,
ProjectConverterImpl.class
})
分別指定我們需要測試的類 ProjectListServiceImpl、ProjectConverter 接口的實現類 ProjectConverterImpl 以及 Mybatis 配置類 MybatisTestConfig。然后通過 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 讓測試類不加載默認的數據源,而是加載我們在 MybatisTestConfig 中配置的數據源;并通過 @PropertySource("classpath:application.properties") 來指定我們使用的測試配置文件。
運行測試類,可以看到 testListAllProjects() 同樣可以測試通過,且 printAllBean() 的結果如下:
========== Spring Beans Total (27) =========
name=cn.asuka.itd.testconfig.MybatisConfig#MapperScannerRegistrar#0, class=class org.mybatis.spring.mapper.MapperScannerConfigurer
name=dataSource, class=class com.alibaba.druid.pool.DruidDataSource
name=hikariPoolDataSourceMetadataProvider, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration$$Lambda/0x00000205685d79a0
name=mybatisConfig, class=class cn.asuka.itd.testconfig.MybatisConfig$$EnhancerBySpringCGLIB$$7108b66c
name=org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory, class=class org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory
name=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
name=org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration
name=org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration, class=class org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration
name=org.springframework.boot.context.internalConfigurationPropertiesBinder, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBinder
name=org.springframework.boot.context.internalConfigurationPropertiesBinderFactory, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBinder$Factory
name=org.springframework.boot.context.properties.BoundConfigurationProperties, class=class org.springframework.boot.context.properties.BoundConfigurationProperties
name=org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor, class=class org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
name=org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter, class=class org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter$$Lambda/0x00000205685d7bb8
name=org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration, class=class org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration
name=org.springframework.boot.test.context.ImportsContextCustomizer$ImportsCleanupPostProcessor, class=class org.springframework.boot.test.context.ImportsContextCustomizer$ImportsCleanupPostProcessor
name=org.springframework.boot.test.mock.mockito.MockitoPostProcessor, class=class org.springframework.boot.test.mock.mockito.MockitoPostProcessor
name=org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor, class=class org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor
name=org.springframework.context.annotation.internalAutowiredAnnotationProcessor, class=class org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
name=org.springframework.context.annotation.internalCommonAnnotationProcessor, class=class org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
name=org.springframework.context.annotation.internalConfigurationAnnotationProcessor, class=class org.springframework.context.annotation.ConfigurationClassPostProcessor
name=org.springframework.context.event.internalEventListenerFactory, class=class org.springframework.context.event.DefaultEventListenerFactory
name=org.springframework.context.event.internalEventListenerProcessor, class=class org.springframework.context.event.EventListenerMethodProcessor
name=projectConverterImpl, class=class cn.asuka.itd.converter.ProjectConverterImpl
name=projectListServiceImpl, class=class cn.asuka.itd.project.service.impl.ProjectListServiceImpl
name=projectMapper, class=class jdk.proxy2.$Proxy84
name=spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties, class=class org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
name=sqlSessionFactory, class=class org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
可以看到只加載了 27 個 Bean,大大減少了 Bean 的加載數量,對測試運行速度提升也有幫助。
總結
如果需要指定需要測試的 Bean 及其依賴,而不是加載完整的上下文環境,可以在 @SpringBootTest 注解的 classes 參數中配置需要測試及依賴的類或對象。如果遇到不是項目中自己寫的或者可以自動生成的實現類,可以通過配置 @TestConfiguration 的方式,在測試配置中注冊相關的 Bean。最終做到縮小測試范圍,提高測試運行效率。

浙公網安備 33010602011771號