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上获得 。