无需重新加载上下文即可编辑并重新运行springboot单元测试以加快测试速度
我有一个 Spring Boot 应用程序,并使用 postgres 测试容器 ( https://www.testcontainers.org/ ) 和 JUnit编写了单元测试。测试具有 @SpringBootTest 注释,它在运行测试之前加载上下文并启动测试容器。
在我相对较旧的 Macbook 上加载上下文并启动容器大约需要 15 秒,但测试本身非常快(每个 < 100 毫秒)。因此,在包含 100 次测试的完整构建中,这并不重要。这是 15 秒的一次性成本。但是在 IDE 中单独开发/调试测试变得非常缓慢。每一次测试都会产生 15 秒的启动成本。
我知道 IntelliJ 和 Springboot 支持在应用程序运行时热重载类。对于单元测试是否有类似的解决方案/建议?即保持上下文加载和 testcontainer(DB) 运行,但只重新编译修改后的测试类并再次运行选定的测试。
回答
我相信您的问题有一个简单的解决方案。您尚未指定在测试中究竟如何运行测试容器,但是我有使用以下方法的成功经验:
对于在本地运行的测试 - 在您的笔记本电脑上启动 postgres 服务器一次(比如在您工作日开始时或某事)。它可以是 dockerized 进程,甚至可以是常规的 postgresql 安装。
在测试期间,spring boot 应用程序并不真正知道它与测试容器交互——它获取主机/端口/凭据,仅此而已——它从这些参数中创建了一个数据源。
因此,对于您的本地开发,您可以修改与测试容器的集成,以便只有在没有“LOCAL.TEST.MODE”环境时才会启动实际的测试容器。变量定义(基本上你可以选择任何名称 - 它不存在)。
然后,在您的笔记本电脑上定义 ENV 变量(或者您可以为此使用系统属性 - 任何对您更有效的方法),然后配置 spring boot 的数据源以获取本地安装的属性(如果定义了该系统属性):
简而言之,它可以是这样的:
@Configuration
@ConditionalOnProperty(name = "test.local.mode", havingValue = "true", matchIfMissing = false)
public class MyDbConfig {
@Bean
public DataSource dataSource () {
// create a data source initialized with local credentials
}
}
当然,可以实现更多带有配置属性的“聪明”解决方案,这完全取决于您如何与测试容器集成以及数据源初始化的实际属性来自哪里,但想法将保持不变:
在您当地的环境中。您实际上将使用本地安装的 PostgreSQL 服务器,甚至不会启动测试容器
由于 postgresql 中的所有操作包括 DDL 都是事务性的,您可以@Transactional在测试上添加注释,spring 将回滚由测试以便数据库不会充满垃圾数据。
与测试容器相反,这种方法有一个显着的优势:
如果您的测试失败并且一些数据保留在数据库中,您可以在本地进行检查,因为服务器将保持活动状态。因此,您将能够使用 PG Admin 或其他方式连接到数据库并检查状态...
更新 1
基于 op 的评论
我明白你的意思,基本上,你提到了两个不同的问题,我将尝试分别提及
问题 1 应用程序上下文大约需要 10-12 秒才能启动。
好吧,这是需要调查的事情。可能有一些 bean 初始化缓慢。所以你应该明白为什么应用程序启动这么慢:
Spring 的代码(扫描、bean 定义填充等)适用于一秒钟的粒子,并且本身通常不是瓶颈- 它必须在您的应用程序中的某个地方。
检查 bean 启动时间有点超出这个问题的范围,尽管肯定有这样做的方法,例如:
请参阅此线程和更新的 spring 版本,如果您在此处使用执行器this。所以我假设你会想出一种或另一种方式为什么它开始缓慢
无论如何,您可以使用此类信息做什么,以及如何使应用程序上下文加载过程更快?好吧,显然您可以从配置中排除慢速 bean/bean 集,也许您在测试中根本不需要它,或者至少可以使用它@MockBean- 这取决于实际用例。也可以在某些情况下提供配置,该配置仍将加载该慢速 bean,但会改变其行为,使其不会变慢。
我还可以指出“普遍适用的想法”,无论您的实际代码库如何,它们都可以提供帮助。
首先,如果您正在运行共享完全相同配置的不同测试用例(IDE 中的多选测试并一次运行它们),那么 Spring Boot 足够智能,不会重新初始化应用程序上下文。这称为“缓存中的应用程序上下文缓存”。这是有关此主题的众多教程之一。
另一种方法是使用惰性 bean 初始化。在 spring 2.2+ 中有一个属性
spring:
main:
lazy-initialization: true
当然,如果您不打算在生产中使用它,请在src/test/resource您选择的配置文件中定义它。只要遵守命名约定,spring-boot 也会在测试期间读取它。如果您对此有技术问题。(再次超出问题范围),然后考虑阅读本教程
如果您的 Spring Boot 早于 2.2,您可以尝试“手动”执行此操作:以下是方法
我想提到的最后一个方向是 - 重新考虑您的测试实现。如果您有一个大项目要测试,这一点尤其重要。通常,应用程序具有分层,例如服务、DAO-s、控制器,您知道。我的观点是,涉及 DB 的测试应该只用于 DAO 层——这是您测试 SQL 查询的地方。业务逻辑代码通常不需要数据库连接,并且通常可以覆盖在根本不使用 spring 的单元测试中。因此,不是使用@SpringBootTest启动整个应用程序上下文的注释,您只能运行 DAO(s) 的配置,这将启动更快的机会和“慢豆”属于应用程序的其他部分。@DataJpaTest.
这是基于整个 spring 测试包仅用于集成测试的想法,一般来说,启动 spring 的测试是集成测试,并且您可能更喜欢尽可能使用单元测试,因为它们是速度更快,并且不使用外部依赖项:数据库、远程服务等。
第二个问题:schema经常不同步
在我目前的方法中,测试容器启动,liquibase 应用我的模式,然后执行测试。一切都在 IDE 内完成,这样更方便一些。
我承认我没有使用过 liquibase,我们使用了 flyway,但我相信答案是一样的。
简而言之 - 这将继续像这样工作,你不需要改变任何东西。
我来解释一下。
Liquibase 应该从 spring 应用程序上下文开始,它应该应用迁移,这是真的。但是在实际应用迁移之前,它应该检查是否已经应用了迁移,如果数据库是同步的,它什么都不做。Flyway 为此目的在数据库中维护一个表,我确定 liquibase 使用类似的机制。
因此,只要您不创建表或进行测试的内容,就应该很高兴:
假设您是第一次启动 Postgres 服务器,您在“工作日开始时”运行的第一个测试,按照上述用例将创建一个架构并部署所有表、索引等。在 liquibase 迁移的帮助下,然后将开始测试。
但是,现在当您开始第二个测试时 - 将已经应用迁移。这相当于在非测试场景(暂存,生产等等)中重新启动应用程序本身 - 重新启动本身不会真正将所有迁移应用到数据库。这里也一样...
好的,这是最简单的情况,但是您可能会在测试中填充数据(好吧,您应该是 😉 )这就是为什么我提到有必要@Transactional在原始答案中对测试本身进行注释。
此注释在运行测试中的所有代码之前创建一个事务并人为地将其回滚 - 读取,删除测试中填充的所有数据,尽管测试已通过
现在让它变得更复杂,如果您创建表,在测试中更改现有表上的列会怎样?好吧,即使对于生产场景,仅凭这一点就会使您的 liquibase 变得疯狂,所以您可能不应该这样做,但是再次@Transactional进行测试本身会有所帮助,因为 PostgreSQL 的 DDL(只是为了澄清 DDL = 数据定义语言,所以我的意思是命令像ALTER TABLE,基本上任何改变现有架构的东西)命令也是事务性的。我知道例如 Oracle 没有在事务中运行 DDL 命令,但从那时起事情可能已经发生了变化。