Spring @DynamicPropertySource指南
- Spring
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之類的技術,則我們的測試工作流程將如下所示:
- 在進行所有測試之前,請先設置一個組件,例如PostgreSQL。通常,這些組件偵聽隨機端口。
- 運行測試。
- 拆下組件。
如果我們的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();
}
}
在這裡,我們正在實現AfterAllCallback和BeforeAllCallback來創建JUnit 5擴展。這樣,JUnit 5將在運行所有測試之前beforeAll()
邏輯,並在運行測試之後執行afterAll()
使用這種方法,我們的測試代碼將變得乾淨:
@SpringBootTest
@ExtendWith(PostgreSQLExtension.class)
public class ArticleTestFixtureLiveTest {
// just the test code
}
除了更具可讀性之外,我們只需添加@ExtendWith(PostgreSQLExtension.class)
批註即可輕鬆重用相同的功能。不需要像其他兩種方法那樣在需要的任何地方復制粘貼整個PostgreSQL設置。
六,結論
在本教程中,我們首先看到測試依賴於數據庫之類的Spring組件有多困難。然後,我們針對此問題引入了三種解決方案,每種解決方案都在先前解決方案必須提供的基礎上進行了改進。