使用SpringBoot和Testcontainers进行数据库集成测试| Baeldung


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