Spring Data JPA提供了一种创建数据库查询并使用嵌入式H2数据库进行测试的简便方法。
但在某些情况下,对真实数据库进行测试会更有利可图,特别是如果我们使用依赖于提供程序的查询。
在本教程中,我们将演示如何使用 Testcontainers 与Spring Data JPA和PostgreSQL数据库进行集成测试。
在我们之前的教程中,我们 主要使用@Query注释 创建了一些数据库 查询 ,我们现在将对其进行测试。
要在我们的测试中使用PostgreSQL数据库,我们必须添加 Testcontainers依赖 与测试范围和 PostgreSQL驱动 我们的pom.xml:
<dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.10.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
我们还在test resources目录下创建一个application.properties文件,在该目录中我们指示Spring使用正确的驱动程序类,并在每次测试运行时创建和删除该方案:
spring.datasource.driver-<b>class</b>-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop
. 单一测试用法
要在单个测试类中开始使用PostgreSQL实例,我们必须首先创建容器定义,然后使用其参数建立连接:
@RunWith(SpringRunner.<b>class</b>) @SpringBootTest @ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.<b>class</b>}) <b>public</b> <b>class</b> UserRepositoryTCIntegrationTest <b>extends</b> UserRepositoryCommonIntegrationTests { @ClassRule <b>public</b> <b>static</b> PostgreSQLContainer postgreSQLContainer = <b>new</b> PostgreSQLContainer(<font>"postgres:11.1"</font><font>) .withDatabaseName(</font><font>"integration-tests-db"</font><font>) .withUsername(</font><font>"sa"</font><font>) .withPassword(</font><font>"sa"</font><font>); <b>static</b> <b>class</b> Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { <b>public</b> <b>void</b> initialize(ConfigurableApplicationContext configurableApplicationContext) { TestPropertyValues.of( </font><font>"spring.datasource.url="</font><font> + postgreSQLContainer.getJdbcUrl(), </font><font>"spring.datasource.username="</font><font> + postgreSQLContainer.getUsername(), </font><font>"spring.datasource.password="</font><font> + postgreSQLContainer.getPassword() ).applyTo(configurableApplicationContext.getEnvironment()); } } } </font>
在上面的示例中,我们使用 JUnit中的@ClassRule 在执行测试方法之前设置数据库容器。我们还创建了一个实现ApplicationContextInitializer的静态内部类 。 作为最后一步,我们将@ContextConfiguration批注应用于我们的测试类,初始化类作为参数。
通过执行这三个操作,我们可以在发布Spring上下文之前设置连接属性。
被测试的用例:
@Modifying @Query(<font>"update User u set u.status = :status where u.name = :name"</font><font>) <b>int</b> updateUserSetStatusForName(@Param(</font><font>"status"</font><font>) Integer status, @Param(</font><font>"name"</font><font>) String name); @Modifying @Query(value = </font><font>"UPDATE Users u SET u.status = ? WHERE u.name = ?"</font><font>, nativeQuery = <b>true</b>) <b>int</b> updateUserSetStatusForNameNative(Integer status, String name); </font>
使用配置的环境测试它们:
@Test @Transactional <b>public</b> <b>void</b> givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){ insertUsers(); <b>int</b> updatedUsersSize = userRepository.updateUserSetStatusForName(0, <font>"SAMPLE"</font><font>); assertThat(updatedUsersSize).isEqualTo(2); } @Test @Transactional <b>public</b> <b>void</b> givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){ insertUsers(); <b>int</b> updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, </font><font>"SAMPLE"</font><font>); assertThat(updatedUsersSize).isEqualTo(2); } <b>private</b> <b>void</b> insertUsers() { userRepository.save(<b>new</b> User(</font><font>"SAMPLE"</font><font>, </font><font>"email@example.com"</font><font>, 1)); userRepository.save(<b>new</b> User(</font><font>"SAMPLE1"</font><font>, </font><font>"email2@example.com"</font><font>, 1)); userRepository.save(<b>new</b> User(</font><font>"SAMPLE"</font><font>, </font><font>"email3@example.com"</font><font>, 1)); userRepository.save(<b>new</b> User(</font><font>"SAMPLE3"</font><font>, </font><font>"email4@example.com"</font><font>, 1)); userRepository.flush(); } </font>
在上面的场景中,第一个测试以成功结束,但第二个测试抛出 InvalidDataAccessResourceUsageException 并显示以下消息:
Caused by: org.postgresql.util.PSQLException: ERROR: column <font>"u"</font><font> of relation </font><font>"users"</font><font> does not exist </font>
如果我们使用H2嵌入式数据库运行相同的测试,则两个测试都将成功完成,但PostgreSQL不接受SET子句中的别名。我们可以通过删除有问题的别名来快速修复查询:
@Modifying @Query(value = <font>"UPDATE Users u SET status = ? WHERE u.name = ?"</font><font>, nativeQuery = <b>true</b>) <b>int</b> updateUserSetStatusForNameNative(Integer status, String name); </font>
这次两次测试都成功完成。在此示例中,我们使用Testcontainers来识别本机查询的问题,否则在切换到生产中的真实数据库之后会显示该问题。我们还应该注意到,使用JPQL查询通常更安全,因为Spring会根据所使用的数据库提供程序正确地进行转换。
共享数据库实例
在上一段中,我们描述了如何在单个测试中使用Testcontainers。在实际情况中,由于启动时间相对较长,我们希望在多个测试中重用相同的数据库容器。
现在让我们通过扩展PostgreSQLContainer 并覆盖 start()和stop()方法来创建数据库容器创建的公共类:
<b>public</b> <b>class</b> BaeldungPostgresqlContainer <b>extends</b> PostgreSQLContainer<BaeldungPostgresqlContainer> { <b>private</b> <b>static</b> <b>final</b> String IMAGE_VERSION = <font>"postgres:11.1"</font><font>; <b>private</b> <b>static</b> BaeldungPostgresqlContainer container; <b>private</b> BaeldungPostgresqlContainer() { <b>super</b>(IMAGE_VERSION); } <b>public</b> <b>static</b> BaeldungPostgresqlContainer getInstance() { <b>if</b> (container == <b>null</b>) { container = <b>new</b> BaeldungPostgresqlContainer(); } <b>return</b> container; } @Override <b>public</b> <b>void</b> start() { <b>super</b>.start(); System.setProperty(</font><font>"DB_URL"</font><font>, container.getJdbcUrl()); System.setProperty(</font><font>"DB_USERNAME"</font><font>, container.getUsername()); System.setProperty(</font><font>"DB_PASSWORD"</font><font>, container.getPassword()); } @Override <b>public</b> <b>void</b> stop() { </font><font><i>//do nothing, JVM handles shut down</i></font><font> } } </font>
通过将 stop()方法留空,我们允许JVM处理容器关闭。我们还实现了一个简单的单例模式,其中只有第一个测试触发容器启动,每个后续测试使用现有实例。在 start()方法中,我们使用 System#setProperty 将连接参数设置为环境变量。
我们现在可以将它们放在application.properties 文件中:
spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD}
现在让我们在测试定义中使用我们的实用程序类:
@RunWith(SpringRunner.<b>class</b>) @SpringBootTest <b>public</b> <b>class</b> UserRepositoryTCAutoIntegrationTest { @ClassRule <b>public</b> <b>static</b> PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance(); <font><i>// tests</i></font><font> } </font>
与前面的示例一样,我们将@ClassRule 注释应用于包含容器定义的字段。这样,在创建Spring上下文之前,将使用正确的值填充DataSource连接属性。
现在,我们只需定义一个使用BaeldungPostgresqlContainer 实用程序类实例化的@ClassRule注释字段, 就可以使用相同的数据库实例实现多个测试。
结论
在本文中,我们介绍了使用Testcontainers对真实数据库实例执行测试的方法。
我们使用Spring 的ApplicationContextInitializer机制查看单个测试用法的示例 ,以及实现可重用数据库实例化的类。
我们还展示了Testcontainers如何帮助识别多个数据库提供程序的兼容性问题,尤其是对于本机查询。
与往常一样,本文中使用的完整代码可 在GitHub上获得 。