知識庫 / Spring / Spring Boot RSS 訂閱

Spring Boot 和 Testcontainers 中的數據庫集成測試

Persistence,Spring Boot,Testing
HongKong
4
01:26 PM · Dec 06 ,2025

1. 概述

Spring Data JPA 提供了一種便捷的方式來創建數據庫查詢並使用嵌入式 H2 數據庫進行測試。

但在某些情況下,在真實數據庫上進行測試會更有價值,尤其是在我們使用提供商依賴的查詢時。

在本教程中,我們將演示 如何使用 Testcontainers 與 Spring Data JPA 和 PostgreSQL 數據庫進行集成測試

在我們的上一篇教程中,我們主要使用 @Query 註解創建了一些數據庫查詢,現在我們將對其進行測試。

2. 配置

為了在我們的測試中使用 PostgreSQL 數據庫,我們需要添加 Testcontainers 依賴項,並指定 測試 範圍

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

讓我們在測試資源目錄下的 application.properties 文件中創建它,以指示 Spring 使用正確的驅動類併為每次測試運行創建方案:

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

3. 單個測試使用

要在一個單獨的測試類中使用PostgreSQL實例,首先需要創建容器定義,然後使用其參數建立連接:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@ActiveProfiles("tc")
@ContextConfiguration(initializers = {UserRepositoryTCLiveTest.Initializer.class})
public class UserRepositoryTCLiveTest extends UserRepositoryCommon {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
      .withDatabaseName("integration-tests-db")
      .withUsername("sa")
      .withPassword("sa");

    @Test
    @Transactional
    public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
        userRepository.save(new User("SAMPLE", LocalDate.now(), USER_EMAIL, ACTIVE_STATUS));
        userRepository.save(new User("SAMPLE1", LocalDate.now(), USER_EMAIL2, ACTIVE_STATUS));
        userRepository.save(new User("SAMPLE", LocalDate.now(), USER_EMAIL3, ACTIVE_STATUS));
        userRepository.save(new User("SAMPLE3", LocalDate.now(), USER_EMAIL4, ACTIVE_STATUS));
        userRepository.flush();

        int updatedUsersSize = userRepository.updateUserSetStatusForNameNativePostgres(INACTIVE_STATUS, "SAMPLE");

        assertThat(updatedUsersSize).isEqualTo(2);
    }

    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 protected]", 1));
    userRepository.save(new User("SAMPLE1", "[email protected]", 1));
    userRepository.save(new User("SAMPLE", "[email protected]", 1));
    userRepository.save(new User("SAMPLE3", "[email protected]", 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 會根據所使用的數據庫提供商正確地進行翻譯。

3.1. 每個測試使用一個數據庫並進行配置

此前,我們使用 JUnit 4 規則在每個測試類內運行所有測試之前啓動數據庫實例。 最終,這種方法會在每個測試類運行所有測試後啓動數據庫實例並進行清理。

這種方法最大程度上隔離了測試實例。 此外,多次啓動數據庫的開銷可能會導致測試變慢。

除了 JUnit 4 規則的方法外,我們可以修改 JDBC URL,並指示 Testcontainers 為每個測試類創建一個數據庫實例。 這種方法將無需我們編寫任何基礎設施代碼在測試中。

例如,為了重寫上面的示例,我們只需要在我們的 application.properties 中添加以下內容:

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

“tc:” 將會使 Testcontainers 在不修改任何代碼的情況下實例化數據庫實例。因此,我們的測試類將變得如此簡單:

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

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

如果我們將為每個測試類使用一個數據庫實例,那麼這種方法是首選的。

4. 共享數據庫實例

在上一段中,我們描述瞭如何在單個測試中使用 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(classes = Application.class)
@ActiveProfiles({"tc", "tc-auto"})
public class UserRepositoryTCAutoLiveTest extends UserRepositoryCommon {

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

    //tests
}

如前例所示,我們使用 @ClassRule 註解對包含容器定義的字段進行了標註。 這樣,在 Spring 容器創建之前,DataSource 連接屬性就被正確地填充了。

現在我們可以使用相同的數據庫實例實現多個測試,只需定義一個使用@ClassRule 註解標註的字段,並將其實例化為我們的BaeldungPostgresqlContainer 工具類。

5. 結論

本文介紹瞭如何使用 Testcontainers 在真實數據庫實例上進行測試的方法。

我們探討了單次測試的使用示例,以及利用 Spring 的 ApplicationContextInitializer 機制,以及實現一個可重用的數據庫實例化類。

此外,我們還展示了 Testcontainers 如何幫助識別在多個數據庫提供程序之間存在的問題,尤其是在處理原生查詢時。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.