Spring @DynamicPropertySource指南

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 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

 }

如上所示,我們在給定的DynamicPropertyRegistry add(String, Supplier<Object>) Environment添加一些屬性。與我們之前看到的初始化程序相比,這種方法更加簡潔。請注意,使用@DynamicPropertySource註釋的方法必須聲明為static DynamicPropertyRegistry類型的一個參數。

@DynmicPropertySource批註背後的主要動機是為了更輕鬆地促進已經可能的事情。儘管最初設計它是為了與Testcontainer一起使用,但是可以在需要使用動態配置的任何地方使用它。

5.另一種選擇:測試夾具設施

到目前為止,在這兩種方法中,夾具設置和測試代碼都緊密地交織在一起。有時,兩個關注點之間的緊密聯繫使測試代碼複雜化,尤其是當我們需要設置多個內容時。想像一下,如果我們在單個測試中使用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) {

 postgres.stop();

 }

 }

在這裡,我們正在實現AfterAllCallbackBeforeAllCallback來創建JUnit 5擴展。這樣,JUnit 5將在運行所有測試之前beforeAll()邏輯,並在運行測試之後執行afterAll()使用這種方法,我們的測試代碼將變得乾淨:

@SpringBootTest

 @ExtendWith(PostgreSQLExtension.class)

 public class ArticleTestFixtureLiveTest {

 // just the test code

 }

除了更具可讀性之外,我們只需添加@ExtendWith(PostgreSQLExtension.class)批註即可輕鬆重用相同的功能。不需要像其他兩種方法那樣在需要的任何地方復制粘貼整個PostgreSQL設置。

六,結論

在本教程中,我們首先看到測試依賴於數據庫之類的Spring組件有多困難。然後,我們針對此問題引入了三種解決方案,每種解決方案都在先前解決方案必須提供的基礎上進行了改進。