DB Integration Tests with Spring Boot and Testcontainers – 用Spring Boot和Testcontainers进行DB集成测试

最后修改: 2019年 2月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring Data JPA provides an easy way to create database queries and test them with an embedded H2 database.

Spring Data JPA提供了一种简单的方法来创建数据库查询,并通过嵌入的H2数据库来测试它们。

But in some cases, testing on a real database is much more profitable, especially if we use provider-dependent queries.

但是在某些情况下,在真正的数据库上进行测试更有利可图,尤其是当我们使用依赖于供应商的查询时。

In this tutorial, we’ll demonstrate how to use Testcontainers for integration testing with Spring Data JPA and the PostgreSQL database.

在本教程中,我们将演示如何使用Testcontainers进行Spring Data JPA和PostgreSQL数据库的集成测试。

In our previous tutorial, we created some database queries using mainly the @Query annotation, which we’ll now test.

在之前的教程中,我们创建了一些数据库查询,主要使用@Query注释,现在我们来测试一下。

2. Configuration

2.配置

To use the PostgreSQL database in our tests, we have to add the Testcontainers dependency with test scope:

为了在我们的测试中使用PostgreSQL数据库,我们必须添加Testcontainers依赖项,并在test范围内

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.17.3</version>
    <scope>test</scope>
</dependency>

Let’s also create an application.properties file under the test resources directory in which we instruct Spring to use the proper driver class and to create the scheme at each test run:

让我们也在测试资源目录下创建一个application.properties文件,其中我们指示Spring使用适当的驱动类,并在每次测试运行时创建方案。

spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.jpa.hibernate.ddl-auto=create

3. Single Test Usage

3.单次测试使用

To start using the PostgreSQL instance in a single test class, we have to create a container definition first and then use its parameters to establish a connection:

为了在一个测试类中开始使用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());
        }
    }
}

In the above example, we used @ClassRule from JUnit to set up a database container before executing test methods. We also created a static inner class that implements ApplicationContextInitializer. As the last step, we applied the @ContextConfiguration annotation to our test class with the initializer class as a parameter.

在上面的例子中,我们使用了JUnit的@ClassRule来设置数据库容器,然后再执行测试方法。我们还创建了一个实现ApplicationContextInitializer的静态内部类。作为最后一步,我们将@ContextConfiguration注解应用于我们的测试类,并将初始化器类作为一个参数。

By performing these three actions, we can set connection properties before the Spring context is published.

通过执行这三个操作,我们可以在Spring上下文发布之前设置连接属性。

Let’s now use two UPDATE queries from the previous article:

现在让我们使用上一篇文章中的两个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);

And test them with the configured environment:

并用配置好的环境对它们进行测试。

@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();
}

In the above scenario, the first test ends with success but the second throws InvalidDataAccessResourceUsageException with the message:

在上述情况下,第一个测试以成功结束,但第二个测试抛出InvalidDataAccessResourceUsageException的信息。

Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist

If we’d run the same tests using the H2 embedded database, both tests would complete successfully, but PostgreSQL does not accept aliases in the SET clause. We can quickly fix the query by removing the problematic alias:

如果我们使用H2嵌入式数据库运行同样的测试,两个测试都会成功完成,但PostgreSQL不接受SET子句中的别名。我们可以通过删除有问题的别名来快速修复该查询。

@Modifying
@Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

This time both tests complete successfully. In this example, we used Testcontainers to identify a problem with the native query which otherwise would be revealed after switching to a real database on production. We should also notice that using JPQL queries is safer in general because Spring translates them properly depending on the database provider used.

这一次,两个测试都成功完成。在这个例子中,我们使用Testcontainers来识别本地查询的问题,否则在生产中切换到真正的数据库后就会暴露出来。我们还应该注意到,使用JPQL查询一般来说比较安全,因为Spring会根据使用的数据库提供者正确翻译它们。

3.1. One Database per Test with Configuration

3.1.每个测试有一个数据库的配置

So far, we’ve used JUnit 4 rules to spin up a database instance before running all tests inside a test class. Eventually, this approach will create a database instance before each test class and tear it down after running all tests in each class.

到目前为止,我们已经使用JUnit 4规则在运行测试类中的所有测试之前启动了一个数据库实例。最终,这种方法将在每个测试类之前创建一个数据库实例,并在运行每个类中的所有测试后将其拆下。

This approach creates maximum isolation between the test instances. Also, the overhead of launching a database multiple times can make tests slow.

这种方法在测试实例之间创造了最大的隔离性。另外,多次启动数据库的开销会使测试变得缓慢。

In addition to the JUnit 4 rules approach, we can modify the JDBC URL and instruct the Testcontainers to create a database instance per test class. This approach will work without requiring us to write some infrastructural code in our tests.

除了JUnit 4规则的方法外,我们可以修改JDBC URL并指示Testcontainers为每个测试类创建一个数据库实例。这种方法将在不需要我们在测试中编写一些基础结构代码的情况下发挥作用。

For instance, in order to rewrite the above example, all we have to do is to add this to our application.properties:

例如,为了重写上面的例子,我们所要做的就是在我们的application.properties中添加这个。

spring.datasource.url=jdbc:tc:postgresql:11.1:///integration-tests-db

The “tc:” will make Testcontainers instantiate database instances without any code change. So, our test class would be as simple as:

“tc:”将使Testcontainers实例化数据库实例,而无需改变任何代码。因此,我们的测试类将简单如斯。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserRepositoryTCJdbcLiveTest extends UserRepositoryCommon {

    @Test
    @Transactional
    public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
        // same as above
    }
}

If we’re going to have one database instance per test class, this approach is the preferred one.

如果我们打算每个测试类有一个数据库实例,这种方法是首选。

4. Shared Database Instance

4.共享的数据库实例

In the previous paragraph, we described how to use Testcontainers in a single test. In a real case scenario, we’d like to reuse the same database container in multiple tests because of the relatively long startup time.

在上一段中,我们描述了如何在单个测试中使用Testcontainers。在真实的情况下,由于启动时间相对较长,我们希望在多个测试中重复使用同一个数据库容器。

Let’s now create a common class for database container creation by extending PostgreSQLContainer and overriding the start() and stop() methods:

现在让我们通过扩展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
    }
}

By leaving the stop() method empty, we allow the JVM to handle the container shutdown. We also implement a simple singleton pattern, in which only the first test triggers container startup, and each subsequent test uses the existing instance. In the start() method we use System#setProperty to set connection parameters as environment variables.

通过将stop()方法留空,我们允许JVM处理容器的关闭。我们还实现了一个简单的单子模式,其中只有第一个测试触发了容器的启动,而每个后续的测试都使用现有的实例。在start()方法中,我们使用System#setProperty来设置连接参数作为环境变量。

We can now put them in our application.properties file:

我们现在可以把它们放在我们的application.properties文件中。

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

Let’s now use our utility class in the test definition:

现在让我们在测试定义中使用我们的实用类。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTCAutoIntegrationTest {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();

    // tests
}

As in previous examples, we applied the @ClassRule annotation to a field holding the container definition. This way, the DataSource connection properties are populated with correct values before Spring context creation.

与之前的例子一样,我们将@ClassRule注解应用于持有容器定义的字段。这样一来,DataSource连接属性在Spring上下文创建前就被填充了正确的值。

We can now implement multiple tests using the same database instance simply by defining a @ClassRule annotated field instantiated with our BaeldungPostgresqlContainer utility class.

我们现在可以使用同一个数据库实例实现多个测试,只需定义一个@ClassRule注释的字段,并通过我们的BaeldungPostgresqlContainer实用类实例化。

5. Conclusion

5.结论

In this article, we illustrated ways to perform tests on a real database instance using Testcontainers.

在这篇文章中,我们说明了使用Testcontainers在真实数据库实例上进行测试的方法。

We looked at examples of single test usage, using the ApplicationContextInitializer mechanism from Spring, as well as implementing a class for reusable database instantiation.

我们看了单次测试使用的例子,使用Spring的ApplicationContextInitializer机制,以及实现一个可重复使用的数据库实例化的类。

We also showed how Testcontainers could help in identifying compatibility problems across multiple database providers, especially for native queries.

我们还展示了Testcontainers如何帮助识别跨多个数据库供应商的兼容性问题,特别是对于本地查询。

As always, the complete code used in this article is available over on GitHub.

一如既往,本文中使用的完整代码可在GitHub上找到。