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