Spring Boot 測試中的 Mock @Value
1. 概述
在 Spring Boot 中編寫單元測試時,經常會遇到必須模擬使用@Value
註解載入的外部配置或屬性的情況。這些屬性通常從application.properties
或application.yml
檔案載入並注入到我們的 Spring 元件中。然而,我們通常不希望使用外部文件來載入完整的 Spring 上下文。相反,我們想模擬這些值以保持我們的測試快速且隔離。
在本教程中,我們將了解為什麼以及如何在 Spring Boot 測試中模擬@Value
,以確保測試順利有效,而無需加載整個應用程式上下文。
2. 如何在Spring Boot測試中模擬@Value
讓我們有一個服務類別ValueAnnotationMock
,它使用@Value
從application.properties
檔案取得外部 API URL 的值:
@Service
public class ValueAnnotationMock {
@Value("${external.api.url}")
private String apiUrl;
public String getApiUrl() {
return apiUrl;
}
public String callExternalApi() {
return String.format("Calling API at %s", apiUrl);
}
}
另外,這是我們的application.properties
檔案:
external.api.url=http://dynamic-url.com
那麼,我們如何在測試類別中模擬這個屬性呢?
有多種方法可以模擬@Value
註解。讓我們一一來看看。
2.1.使用@TestPropertySource
註釋
在 Spring Boot 測試中模擬@Value
屬性最簡單的方法是使用@TestPropertySource
註解。這允許我們直接在測試類別中定義屬性。
讓我們透過一個範例來更好地理解:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ValueAnnotationMock.class)
@SpringBootTest
@TestPropertySource(properties = {
"external.api.url=http://mocked-url.com"
})
public class ValueAnnotationMockTestPropertySourceUnitTest {
@Autowired
private ValueAnnotationMock valueAnnotationMock;
@Test
public void givenValue_whenUsingTestPropertySource_thenMockValueAnnotation() {
String apiUrl = valueAnnotationMock.getApiUrl();
assertEquals("http://mocked-url.com", apiUrl);
}
}
在此範例中, @TestPropertySource
為external.api.url
屬性提供模擬值,即http://mocked-url.com
。然後將其註入到ValueAnnotationMock
bean 中。
讓我們探討一下使用@TestPropertySource
來模擬@Value:
- 這種方法允許我們使用 Spring 的屬性注入機制來模擬應用程式的真實行為,這意味著正在測試的程式碼與生產中運行的程式碼很接近。
- 我們可以直接在測試類別中指定特定於測試的屬性值,從而簡化複雜屬性的模擬。
現在,我們來看看這種方法的一些缺點:
- 使用
@TestPropertySource
的測試會載入 Spring 上下文,這可能比不需要上下文載入的單元測試慢。 - 這種方法可能會為簡單的單元測試增加不必要的複雜性,因為它引入了許多 Spring 機制,使測試不那麼孤立並且速度更慢。
2.2.使用ReflectionTestUtils
對於我們想要直接將模擬值注入到private
欄位(例如使用@Value
註解的欄位)的情況,我們可以使用 Spring 的ReflectionTestUtils
類別來手動設定該值。
我們來看看實現:
public class ValueAnnotationMockReflectionUtilsUnitTest {
@Autowired
private ValueAnnotationMock valueAnnotationMock;
@Test
public void givenValue_whenUsingReflectionUtils_thenMockValueAnnotation() {
valueAnnotationMock = new ValueAnnotationMock();
ReflectionTestUtils.setField(valueAnnotationMock, "apiUrl", "http://mocked-url.com");
String apiUrl = valueAnnotationMock.getApiUrl();
assertEquals("http://mocked-url.com", apiUrl);
}
}
此方法繞過了 Spring 的整個上下文,使其非常適合我們根本不想涉及 Spring Boot 的依賴注入機制的純單元測試。
讓我們看看這種方法的一些好處:
- 我們可以直接操作
private
字段,甚至是那些用@Value
註解的字段,而無需修改原始類別。這對於無法輕鬆重構的遺留程式碼很有幫助。 - 它避免載入 Spring 應用程式上下文,從而使測試更快且隔離。
- 我們可以透過在測試過程中動態更改物件的內部狀態來測試確切的行為。
使用ReflectionUtils:
- 透過反射直接存取或修改私有欄位會破壞封裝,這違反了物件導向設計的最佳實踐。
- 雖然它不載入 Spring 的上下文,但由於在執行時間檢查和修改類別結構的開銷,反射比標準存取慢。
2.3.使用建構函數注入
這種方法使用建構函式註入來處理@Value
屬性,為 Spring 上下文注入和單元測試提供了一個乾淨的解決方案,而無需反射或 Spring 的完整環境。
我們有一個主類,其中使用建構函式中的@Value
註解注入apiUrl
和apiPassword
等屬性:
public class ValueAnnotationConstructorMock {
private final String apiUrl;
private final String apiPassword;
public ValueAnnotationConstructorMock(@Value("#{myProps['api.url']}") String apiUrl,
@Value("#{myProps['api.password']}") String apiPassword) {
this.apiUrl = apiUrl;
this.apiPassword = apiPassword;
}
public String getApiUrl() {
return apiUrl;
}
public String getApiPassword() {
return apiPassword;
}
}
它們不是透過字段注入來注入這些值,這可能需要在測試中進行反射或額外的設置,而是直接透過建構函數傳遞。這使得該類別可以輕鬆地使用特定值進行實例化以進行測試。
在測試類別中,我們不需要Spring的上下文來注入這些值。相反,我們可以簡單地用任意值實例化該類別:
public class ValueAnnotationMockConstructorUnitTest {
private ValueAnnotationConstructorMock valueAnnotationConstructorMock;
@BeforeEach
public void setUp() {
valueAnnotationConstructorMock = new ValueAnnotationConstructorMock("testUrl", "testPassword");
}
@Test
public void testDefaultUrl() {
assertEquals("testUrl", valueAnnotationConstructorMock.getApiUrl());
}
@Test
public void testDefaultPassword() {
assertEquals("testPassword", valueAnnotationConstructorMock.getApiPassword());
}
}
這使得測試更加簡單,因為我們不依賴 Spring 的初始化過程。我們可以直接傳遞我們需要的任何值,例如anyUrl
和anyPassword
,來模擬@Value
屬性。
現在,繼續討論使用建構函數注入的一些關鍵優勢:
- 這種方法非常適合單元測試,因為它允許我們繞過對 Spring 上下文的需求。我們可以直接在測試中註入模擬值,使它們更快、更隔離。
- 它簡化了類別構造並確保在構造時注入所有必需的依賴項,使類別更易於推理。
- 使用具有建構函式註入的
final
欄位可確保不變性,使程式碼更安全且更易於偵錯。
接下來,我們回顧一下使用構造函數注入的一些缺點:
- 我們可能需要修改現有程式碼以採用建構函式註入,這可能並不總是可行,特別是對於遺留應用程式。
- 我們需要明確管理測試中的所有依賴項,如果類別具有許多依賴項,這可能會導致冗長的測試設定。
- 如果我們的測試需要處理動態變化的值或大量屬性,建構函式註入可能會變得很麻煩。
三、結論
在本文中,我們看到在 Spring Boot 測試中模擬@Value
是保持單元測試集中、快速且獨立於外部設定檔的常見要求。透過利用@TestPropertySource
和ReflectionTestUtils
等工具,我們可以有效地模擬配置值並維護乾淨、可靠的測試。
透過這些策略,我們可以自信地編寫單元測試來隔離元件的邏輯,而無需依賴外部資源,從而確保更好的測試效能和可靠性。
與往常一樣,所有範例都可以在 GitHub 上找到。