使用Spring Boot和Testcontainer进行数据库集成测试

1.概述


使用Spring Data JPA,您可以轻松创建数据库查询并使用内置的H2数据库对其进行测试。


但是有时在真实数据库上进行测试会有用得多 ,尤其是当我们使用与特定数据库实现相关联的查询时。


在本指南中,我们将展示如何使用Testcontainers与Spring Data JPA和PostgreSQL数据库进行集成测试。


在上一篇文章中,我们主要使用@Query注释创建了几个数据库查询,我们现在对其进行测试。


2.配置


要在测试中使用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> 

我们还将在测试资源目录中创建application.properties文件,在该文件中,我们将设置Spring所需的驱动程序类的使用,并在每次测试运行时创建和删除数据库模式:


 spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop 

3.单元测试


要在单个测试类中开始使用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上下文之前设置连接参数。


现在,我们使用上一篇文章中的两个UPDATE查询:


 @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会根据所使用的数据库提供程序正确地转换它们。


4.共享数据库实例


在上一节中,我们描述了如何在单个测试中使用Testcontainer。 在实际情况下,由于启动时间相对较长,我想在多个测试中使用同一数据库容器。


让我们通过继承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批注简单地设置字段,从而可以使用同一个数据库实例实现多个测试。


5.结论


在本文中,我们展示了使用Testcontainers在生产数据库上的测试方法。


我们还研究了使用Spring的ApplicationContextInitializer机制使用单个测试以及实现用于重用数据库实例的类的示例。


我们还展示了Testcontainer如何帮助识别多个数据库提供程序之间的兼容性问题,尤其是对于本机查询。


与往常一样,本文中使用的完整代码可在GitHub上获得

Source: https://habr.com/ru/post/zh-CN454970/


All Articles