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。最终做到缩小测试范围,提高测试运行效率。
SpringBoot集成测试笔记:缩小测试范围、提高测试效率的更多相关文章
- Charles中使用Rewrite提高测试效率
上次给大家演示了Charles中通过Map Local功能来提高测试效率,Charles还有另外一个强大的功能,Rewrite,这次也给大家演示一下. Charles中的Rewrite功能非常强大,可 ...
- Charles中使用Map Local提高测试效率
书接上回,上次说到Charles中可以使用修改返回值来模拟接口返回,这次我们来说一下Charles中另外一个强大的功能. 我们用手机连接Charles,具体可以参考上一篇<借助Charles来测 ...
- 专注于提高“人肉测试”效率,Bugtags已完成600万元天使轮融资
导语:近日,专注于移动测试的缺陷发现及管理工具“Bugtags”创始人张磊独家透露,Bugtags已完成600万元天使轮投资,投资方为高捷资本. 近日,专注于移动测试的缺陷发现及管理工具“Bugtag ...
- 基于SpringBoot从零构建博客网站 - 整合lombok和mybatis-plus提高开发效率
在上一章节中<技术选型和整合开发环境>,确定了开发的技术,但是如果直接这样用的话,可能开发效率会不高,为了提高开发的效率,这里再整合lombok和mybatis-plus两个组件. 1.l ...
- 读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率
转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting3/ 所谓自动化测试,就是“自动化”+“测试”.自动化本身显然不 ...
- SpringBoot2 集成测试组件,七种测试手段对比
一.背景描述 在版本开发中,时间段大致的划分为:需求,开发,测试: 需求阶段:理解需求做好接口设计: 开发阶段:完成功能开发和对接: 测试上线:自测,提测,修复,上线: 实际上开发阶段两个核心的工作, ...
- SpringBoot学习笔记-Chapter2(hello word)
开篇 第一次在博客园上写博客,初衷是想记录一下学习笔记,以往都是用笔去记录下学习笔记,现在来看在效率.检索速度上以及可可复制性都不好.作为一名Java开发人员 不会Spring Boot一定会被鄙视的 ...
- Mokito 单元测试与 Spring-Boot 集成测试
Mokito 单元测试与 Spring-Boot 集成测试 版本说明 Java:1.8 JUnit:5.x Mokito:3.x H2:1.4.200 spring-boot-starter-test ...
- SQL Server SQL性能优化之--通过拆分SQL提高执行效率,以及性能高低背后的原因
复杂SQL拆分优化 拆分SQL是性能优化一种非常有效的方法之一, 具体就是将复杂的SQL按照一定的逻辑逐步分解成简单的SQL,借助临时表,最后执行一个等价的逻辑,已达到高效执行的目的 一直想写一遍通过 ...
- 成吨提高开发效率:Intellij Shortcuts精简子集与思维模式
在线精简cheatsheet备查表:intellij.linesh.twGithub项目:intellij-mac-frequent-keymap Intellij的快捷键多而繁杂,从官方推荐的key ...
随机推荐
- 分页工具之【PageHelper】
1.PageHelper技术 依赖 <!-- PageHelper --> <dependency> <groupId>com.github.pagehelper& ...
- 不同版本AutoCAD的ProgID
AutoCAD产品名 ProgID AutoCAD 2004 AutoCAD.Application.16 AutoCAD 2005 AutoCAD.Application.16.1 AutoCAD ...
- vs2017试用期结束问题
Visual Studio 2017 企业版 Enterprise 更新码:NJVYC-BMHX2-G77MM-4XJMR-6Q8QF
- 玩转代码:深入GitHub,高效管理我们的“shou学”平台源代码
玩转代码:深入GitHub,高效管理我们的"shou学"平台源代码 在当今快节奏的开发世界中,有效地管理代码不仅仅是一种良好实践,更是一种必需.无论您是独立开发者还是大型团队的一员 ...
- 2、redis设置密码【永久性设置密码】
1)编辑Redis的配置文件 /usr/local/etc/redis.conf 打开Finder,按 Command + Shift + G 快捷键打开"前往文件夹"对话框,在弹 ...
- Spring注解之@Async:Spring Boot实现异步调用
前言 在日常开发过程中,会遇到一些需求是和主业务逻辑低耦合的,不要求和主业务逻辑同步进行,比如记录日志信息.发送消息通知电子邮件.生成PDF合同和导出报表等需求,而且,这些需求往往处理起来比较耗时.这 ...
- 面试题-简单介绍 Spring MVC 执行流程或者Spring MVC的工作原理
摘要 简单介绍一下当用户在客户端发起请求后,Spring MVC 的执行流程,提升自己对Spring框架的认知层次.一言以蔽之,解答[Spring MVC的工作原理是什么],这是一个热点面试题. ...
- 腾讯云对象存储工具类和demo
使用 package com.ruoyi; import java.io.File; public class mytest { private static String secretId = &q ...
- 从零开始学Flink:揭开实时计算的神秘面纱
一.为什么需要Flink? 当你在电商平台秒杀商品时,1毫秒的延迟可能导致交易失败:当自动驾驶汽车遇到障碍物时,10毫秒的计算延迟可能酿成事故.这些场景揭示了一个残酷事实:数据的价值随时间呈指数级衰减 ...
- VKProxy已提供命令行工具,镜像和简单的ui
VKProxy 是使用c#开发的基于 Kestrel 实现 L4/L7的代理 经过6个月业余时间偶尔缝缝补补,已经达到能跑的地步了 (感兴趣的同学烦请点个github小赞赞呢) 相关使用写了一些简单的 ...