本系列的第三篇文章以及主要系列的一个小分支-这次,我将展示Spring Integration Testing库如何工作以及它是如何工作的,在测试开始时会发生什么,以及如何微调应用程序及其测试环境。
Hixon10 注释提示我写这篇文章,内容涉及如何在集成测试中使用真实的基础,例如Postgres。 评论作者建议使用便利的全包式库Embedded-database-spring-test 。 并且我已经在代码中添加了一个段落和一个使用示例,但是后来我想到了。 当然,使用现成的库是正确且良好的,但是如果目标是了解如何为Spring应用程序编写测试 ,那么展示如何自己实现相同的功能将更加有用。 首先,这是谈论Spring Test内幕的重要原因。 其次,我相信您不能依赖第三方库,如果您不了解它们在内部的排列方式,只会导致强化技术“魔术”神话。
这次将没有用户功能,但将有一个需要解决的问题- 我想在随机端口上启动真实数据库并将应用程序自动连接到该临时数据库,并且在测试后我停止并删除数据库。
首先,按照惯例,有些理论。 对于不太熟悉bin,上下文,配置概念的人们,我建议您刷新知识,例如,在我的文章The Spring / Habr的反面 。
春季测试
Spring Test是Spring框架中包含的库之一,实际上,有关集成测试的文档部分中描述的所有内容都与此相关。 该库解决的四个主要任务是:
- 管理Spring IoC容器及其在测试之间的缓存
- 提供测试类的依赖注入
- 提供适合集成测试的事务管理
- 提供一组基类,以帮助开发人员编写集成测试
我强烈建议您阅读官方文档,其中包含许多有用和有趣的内容。 在这里,我将快速介绍一些实用的提示,以帮助您牢记。
测试生命周期

测试的生命周期如下所示:
- 测试框架的扩展(用于JUnit 4的
SpringExtension
和用于JUnit 5的SpringExtension
)调用Test Context Bootstrapper - Boostrapper创建
TestContext
存储测试和应用程序当前状态的主类 TestContext
设置不同的钩子(例如在测试之前启动事务,在测试之后回滚),将依赖项注入测试类(测试类上的所有@Autowired
字段)并创建上下文- 使用上下文加载器创建上下文 -它采用应用程序的基本配置,并将其与测试配置合并(重叠的属性,配置文件,容器,初始化程序等)。
- 使用完整描述应用程序的复合键(一组容器,属性等)缓存上下文。
- 试运行
实际上,所有管理测试的工作都是通过spring-test
,而Spring Boot Test
又会添加几个帮助器类,例如熟悉的@DataJpaTest
和@SpringBootTest
和有用的实用程序(如TestPropertyValues
来动态更改上下文属性。 它还允许您将应用程序作为真实的Web服务器或模拟环境(无法通过HTTP访问)运行,使用@MockBean
等擦除系统组件非常方便。
上下文缓存
集成测试中引起很多问题和误解的一个非常晦涩的主题之一可能是测试之间的上下文缓存 (请参阅上面的第5段)及其对测试速度的影响。 我经常听到的一个评论是,集成测试“缓慢”并且“为每个测试运行应用程序”。 因此,它们确实可以运行-但并非每次测试都可以。 每个上下文(即应用程序实例)将得到最大程度的重用,即 如果10个测试使用相同的应用程序配置,则该应用程序将对所有10个测试启动一次。 应用程序的“相同配置”是什么意思? 对于Spring Test,这意味着Bean,配置类,概要文件,属性等的集合未更改。 实际上,这意味着,例如,这两个测试将使用相同的上下文:
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { }
缓存中的上下文数限制为32-此外,根据LRSU原理,将从缓存中删除其中一个上下文。
是什么可以阻止Spring Test重用缓存中的上下文并创建一个新的上下文?
@DirtiesContext
最简单的选项是,如果测试带有注释标记,则不会缓存上下文。 如果测试更改了应用程序的状态并且您想“重置”它,这将很有用。
@MockBean
一个非常不明显的选项,我什至单独渲染了它-@MockBean用可以通过Mockito测试的模拟代替了上下文中的真实bean(在以下文章中,我将展示如何使用它)。 关键是该注释更改了应用程序中的bean集合,并强制Spring Test创建新的上下文。 例如,如果我们以前面的示例为例,将在此处创建两个上下文:
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; }
@TestPropertySource
任何属性更改都会自动更改缓存键,并创建一个新的上下文。
@ActiveProfiles
更改活动配置文件也会影响缓存。
@ContextConfiguration
当然,任何配置更改也将创建新的上下文。
我们开始基地
因此,现在借助所有这些知识,我们将尝试 起飞 了解如何以及在何处可以运行数据库。 这里没有一个正确的答案,它取决于要求,但是您可以想到两个选择:
- 在类中的所有测试之前运行一次。
- 为每个缓存的上下文(可能多个类)运行一个随机实例和一个单独的数据库。
根据要求,您可以选择任何选项。 如果以我为例,Postgres相对较快地启动,并且第二个选项看起来合适,那么第一个选项可能适合更困难的事情。
第一个选项不依赖于Spring,而是依赖于测试框架。 例如,您可以对JUnit 5进行扩展 。
如果将有关测试库,上下文和缓存的所有知识放到一起,则任务归结为以下内容: 创建新的应用程序上下文时,需要在随机端口上运行数据库并将连接数据传输到上下文 。
ApplicationContextInitializer
接口负责在Spring中启动之前对上下文执行操作。
ApplicationContextInitializer
该接口只有一个initialize
方法,该方法在“启动”上下文之前(即在调用refresh
方法之前)执行,并且允许您对上下文进行更改-添加容器,属性。
就我而言,该类如下所示:
public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } }
发生的第一件事是从yandex-qatools / postgresql嵌入式库启动嵌入式Postgres。 然后,创建一组属性-新启动的基础的JDBC URL,驱动程序类型和方案的Hibernate行为(自动创建)。 一个不明显的事情就是spring.test.database.replace=none
这就是告诉我们的数据,我们不必尝试连接到嵌入式数据库(例如H2),也不需要替换DataSource bin(这是可行的)。
另外一个要点是application.registerBean(…)
。 通常,此bean当然不能注册-如果没有人在应用程序中使用它,则不是特别需要。 仅需要注册来指定销毁上下文时Spring将调用的destroy方法,在我的情况下,此方法将调用postgres.stop()
并停止数据库。
总的来说,魔术就结束了,如果有的话。 现在,我将在测试上下文中注册此初始化程序:
@DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ...
甚至为了方便起见,您可以创建自己的注释,因为我们都喜欢注释!
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { }
现在,任何由@EmbeddedPostgrestTest
注释的测试都将在一个随机端口上启动一个具有随机名称的数据库,将Spring配置为连接到该数据库,并在测试结束时将其停止。
@EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... }
结论
我想证明在Spring中没有神秘的魔术,只有很多“智能”和灵活的内部机制,但是知道了它们,您就可以完全控制测试和应用程序本身。 通常,在战斗项目中,我不会激励每个人编写用于设置测试集成环境的方法和类,如果有现成的解决方案,则可以采用。 尽管如果整个方法是5行代码,那么可能会不必要地将依赖项拖到项目中,尤其是不了解实现。
指向本系列其他文章的链接