知識庫 / Spring RSS 訂閱

動態屬性源指南(Spring)

Spring
HongKong
9
12:47 PM · Dec 06 ,2025

1. 概述

當今的應用通常不獨立運行:我們通常需要連接到各種外部組件,例如 PostgreSQL、Apache Kafka、Cassandra、Redis 以及其他外部 API。

在本教程中,我們將瞭解 Spring Framework 5.2.5 如何通過引入 動態屬性,簡化這些應用程序的測試。

首先,我們將定義問題並瞭解我們以前如何以不太理想的方式解決這個問題。然後,我們將介紹 @DynamicPropertySource 註解,並瞭解它如何為相同的問題提供更好的解決方案。最後,我們還將研究來自測試框架的另一種解決方案,該解決方案可能優於純 Spring 解決方案。

2. 問題:動態屬性

假設我們正在開發一個典型的應用程序,該應用程序使用 PostgreSQL 作為其數據庫。我們首先從一個簡單的 JPA 實體開始:

@Entity
@Table(name = "articles")
public class Article {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    private String title;

    private String content;

    // getters and setters
}

為了確保該實體按預期工作,我們應該編寫一個測試用例來驗證其數據庫交互。由於該測試用例需要與實際數據庫進行交互,我們應該事先設置一個 PostgreSQL 實例。

在測試執行過程中,有不同的方法可以設置此類基礎設施工具。事實上,這些解決方案可以分為三大類:

  • 設置一個單獨的數據庫服務器,僅用於測試
  • 使用一些輕量級的、特定於測試的替代方案或假數據(如 H2)
  • 讓測試用例本身管理數據庫的生命週期

由於我們不應將測試與生產環境區分開來,因此與使用測試雙(如 H2)相比,第三種選擇提供了更好的隔離。 此外,藉助諸如 Docker 和 Testcontainers 之類的技術,很容易實現第三種選擇。

以下是使用諸如 Testcontainers 之類技術時的測試工作流程:

  1. 在所有測試之前設置一個組件,例如 PostgreSQL。 通常,這些組件會監聽隨機端口。
  2. 運行測試。
  3. 銷燬組件。

如果我們的 PostgreSQL 容器將每次都監聽隨機端口,那麼我們應該通過動態的方式設置和更改 spring.datasource.url 配置屬性。 基本上,每個測試用例都應該具有自己的 spring.datasource.url 配置屬性版本。

當配置是靜態的,我們就可以使用 Spring Boot 的配置管理功能輕鬆管理它們。 但是,當我們面臨動態配置時,相同的任務可能會變得具有挑戰性。

現在我們知道這個問題,讓我們看看傳統的解決方案。

3. 傳統解決方案

實施動態屬性的首選方法是使用自定義的 ApplicationContextInitializer。 基本上,我們首先設置好基礎設施,然後利用第一步的信息來定製 ApplicationContext

@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = ArticleTraditionalLiveTest.EnvInitializer.class)
class ArticleTraditionalLiveTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
      .withDatabaseName("prop")
      .withUsername("postgres")
      .withPassword("pass")
      .withExposedPorts(5432);

    static class EnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            TestPropertyValues.of(
              String.format("spring.datasource.url=jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()),
              "spring.datasource.username=postgres",
              "spring.datasource.password=pass"
            ).applyTo(applicationContext);
        }
    }

    // omitted 
}

讓我們逐步瞭解這個相對複雜的配置。JUnit 會在其他任何操作之前創建並啓動容器。容器準備就緒後,Spring 擴展將調用初始化器來將動態配置應用於 Spring Environment顯然,這種方法有點冗長且複雜。

只有在完成這些步驟之後,我們才能編寫測試。

@Autowired
private ArticleRepository articleRepository;

@Test
void givenAnArticle_whenPersisted_thenShouldBeAbleToReadIt() {
    Article article = new Article();
    article.setTitle("A Guide to @DynamicPropertySource in Spring");
    article.setContent("Today's applications...");

    articleRepository.save(article);

    Article persisted = articleRepository.findAll().get(0);
    assertThat(persisted.getId()).isNotNull();
    assertThat(persisted.getTitle()).isEqualTo("A Guide to @DynamicPropertySource in Spring");
    assertThat(persisted.getContent()).isEqualTo("Today's applications...");
}

4. 使用動態屬性源 (@DynamicPropertySource)

Spring Framework 5.2.5 引入了 @DynamicPropertySource 註解,以簡化添加具有動態值的屬性的方式。 我們的工作只需創建一個帶有 @DynamicPropertySource 註解的靜態方法,並傳入一個 DynamicPropertyRegistry 實例即可。

@SpringBootTest
@Testcontainers
public class ArticleLiveTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
      .withDatabaseName("prop")
      .withUsername("postgres")
      .withPassword("pass")
      .withExposedPorts(5432);

    @DynamicPropertySource
    static void registerPgProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", 
          () -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
        registry.add("spring.datasource.username", () -> "postgres");
        registry.add("spring.datasource.password", () -> "pass");
    }
    
    // tests are same as before
}

如上所示,我們使用 add(String, Supplier<Object>) 方法來向 Spring Environment 添加一些屬性。 這種方法與我們之前看到的 initializer 方法相比,更加簡潔。 請注意,帶有 @DynamicPropertySource 註解的方法必須聲明為 static 且只能接受一個 DynamicPropertyRegistry 類型的參數。

基本上,@DynmicPropertySource 註解的主要目的是更輕鬆地實現一些已經可能的事情。 儘管它最初設計用於與 Testcontainers 配合使用,但我們也可以在需要處理動態配置的任何地方使用它。

5. 替代方案:測試套件

目前,在兩種方法中,fixture 的設置和測試代碼緊密耦合。有時,這種兩項關注點的緊密耦合會使測試代碼變得複雜,尤其是在我們需要設置多個內容時。試想一下,如果我們使用 PostgreSQL 和 Apache Kafka 在單個測試中進行基礎設施設置會是什麼樣子。

此外,基礎設施設置和動態配置將在所有需要它們的測試中重複出現

為了避免這些缺點,我們可以使用大多數測試框架提供的測試套件功能。例如,在 JUnit 5 中,我們可以定義一個擴展程序,在測試類中的所有測試之前啓動 PostgreSQL 實例,配置 Spring Boot,並在測試運行後停止 PostgreSQL 實例:

public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback {

    private PostgreSQLContainer<?> postgres;

    @Override
    public void beforeAll(ExtensionContext context) {
        postgres = new PostgreSQLContainer<>("postgres:11")
          .withDatabaseName("prop")
          .withUsername("postgres")
          .withPassword("pass")
          .withExposedPorts(5432);

        postgres.start();
        String jdbcUrl = String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort());
        System.setProperty("spring.datasource.url", jdbcUrl);
        System.setProperty("spring.datasource.username", "postgres");
        System.setProperty("spring.datasource.password", "pass");
    }

    @Override
    public void afterAll(ExtensionContext context) {
        // do nothing, Testcontainers handles container shutdown
    }
}

在這裏,我們正在實現 <em><a href="https://junit.org/junit5/docs/5.1.1/api/org/junit/jupiter/api/extension/AfterAllCallback.html">AfterAllCallback</a></em><em><a href="https://junit.org/junit5/docs/5.1.1/api/org/junit/jupiter/api/extension/BeforeAllCallback.html">BeforeAllCallback</a></em>,以創建一個 JUnit 5 擴展。 這樣,JUnit 5 將在運行所有測試之前執行beforeAll()邏輯,並在運行測試後執行afterAll()` 方法中的邏輯。 採用這種方法,我們的測試代碼將變得像這樣乾淨:

@SpringBootTest
@ExtendWith(PostgreSQLExtension.class)
@DirtiesContext
public class ArticleTestFixtureLiveTest {
    // just the test code
}

在這裏,我們還添加了 @DirtiesContext 註解到測試類中。重要的是,這 重新創建應用程序上下文,並允許我們的測試類與一個單獨的 PostgreSQL 實例交互,該實例在隨機端口上運行。 從而,我們的測試在完全隔離的情況下執行,並針對一個單獨的數據庫實例。

除了更易讀之外,我們還可以通過簡單地添加 @ExtendWith(PostgreSQLExtension.class) 註解輕鬆重用相同的功能。 不需要我們在需要時複製粘貼整個 PostgreSQL 設置,就像我們在其他兩個方法中做的那樣。

6. 結論

在本教程中,我們首先了解到測試依賴於諸如數據庫之類的組件有多困難。然後,我們介紹了三種解決方案,每種解決方案都比前一種解決方案更勝一籌。

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

發佈 評論

Some HTML is okay.