如何使用 Mockito 模擬構造函數進行單元測試
一、簡介
在這個簡短的教程中,我們將探索使用 Mockito 和 PowerMock 在 Java 中有效模擬構造函數的各種選項。
2. 使用 PowerMock 模擬構造函數
使用 Mockito 3.3 或更低版本不可能模擬構造函數或靜態方法。在這種情況下,像 PowerMock 這樣的庫提供了額外的功能,使我們能夠模擬構造函數的行為並協調它們的交互。
3. 型號
讓我們使用兩個 Java 類來模擬支付處理系統。我們將創建一個PaymentService
類,其中包含處理付款的邏輯,並提供用於指定付款模式的參數化構造函數和具有後備模式的默認構造函數的靈活性:
public class PaymentService {
private final String paymentMode;
public PaymentService(String paymentMode) {
this.paymentMode = paymentMode;
}
public PaymentService() {
this.paymentMode = "Cash";
}
public String processPayment(){
return this.paymentMode;
}
}
PaymentProcessor
類依賴PaymentService
來執行支付處理任務,並提供兩個構造函數,一個用於默認設置,另一個用於自定義支付模式:
public class PaymentProcessor {
private final PaymentService paymentService;
public PaymentProcessor() {
this.paymentService = new PaymentService();
}
public PaymentProcessor(String paymentMode) {
this.paymentService = new PaymentService(paymentMode);
}
public String processPayment(){
return paymentService.processPayment();
}
}
4. 使用 Mockito 模擬默認構造函數
在編寫單元測試時,隔離我們想要測試的代碼至關重要。構造函數經常創建我們不想參與測試的依賴項。模擬構造函數允許我們用模擬對象替換真實對象,確保我們正在測試的行為特定於所檢查的單元。
從 Mockito 3.4 版及更高版本開始,我們可以訪問mockConstruction()
方法。它允許我們模擬對象構造。我們指定要模擬其構造函數的類作為第一個參數。此外,我們以MockInitializer
回調函數的形式提供第二個參數。這個回調函數允許我們在構造過程中定義和操作模擬的行為:
@Test
void whenConstructorInvokedWithInitializer_ThenMockObjectShouldBeCreated(){
try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class,(mock,context)-> {
when(mock.processPayment()).thenReturn("Credit");
})){
PaymentProcessor paymentProcessor = new PaymentProcessor();
Assertions.assertEquals(1,mockPaymentService.constructed().size());
Assertions.assertEquals("Credit", paymentProcessor.processPayment());
}
}
mockConstruction()
方法有多個重載版本,每個版本都適用於不同的用例。在下面的場景中,我們不使用MockInitializer
來初始化模擬對象。我們正在驗證構造函數是否被調用過一次,並且沒有初始化程序可確保構造的PaymentService
對像中paymentMode
字段的null
狀態:
@Test
void whenConstructorInvokedWithoutInitializer_ThenMockObjectShouldBeCreatedWithNullFields(){
try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class)){
PaymentProcessor paymentProcessor = new PaymentProcessor();
Assertions.assertEquals(1,mockPaymentService.constructed().size());
Assertions.assertNull(paymentProcessor.processPayment());
}
}
5. 使用 Mockito 模擬參數化構造函數
在此示例中,我們設置了MockInitializer
並調用了參數化構造函數。我們正在驗證是否創建了一個模擬,並且它具有在初始化期間定義的所需值:
@Test
void whenConstructorInvokedWithParameters_ThenMockObjectShouldBeCreated(){
try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class,(mock, context) -> {
when(mock.processPayment()).thenReturn("Credit");
})){
PaymentProcessor paymentProcessor = new PaymentProcessor("Debit");
Assertions.assertEquals(1,mockPaymentService.constructed().size());
Assertions.assertEquals("Credit", paymentProcessor.processPayment());
}
}
6. 模擬構造函數的範圍
Java 中的 try-with-resources 構造允許我們限制所創建的模擬的範圍。在此塊中,對指定類的公共構造函數的任何調用都會創建模擬對象。當在塊之外的任何地方調用真正的構造函數時,將會調用它。
在下面的示例中,我們沒有定義任何初始值設定項,並多次調用默認構造函數和參數化構造函數。然後,模擬的行為是在構建後定義的。
我們正在驗證三個模擬對象確實已創建並且遵守我們預定義的模擬行為:
@Test
void whenMultipleConstructorsInvoked_ThenMultipleMockObjectsShouldBeCreated(){
try(MockedConstruction<PaymentService> mockPaymentService = Mockito.mockConstruction(PaymentService.class)){
PaymentProcessor paymentProcessor = new PaymentProcessor();
PaymentProcessor secondPaymentProcessor = new PaymentProcessor();
PaymentProcessor thirdPaymentProcessor = new PaymentProcessor("Debit");
when(mockPaymentService.constructed().get(0).processPayment()).thenReturn("Credit");
when(mockPaymentService.constructed().get(1).processPayment()).thenReturn("Online Banking");
Assertions.assertEquals(3,mockPaymentService.constructed().size());
Assertions.assertEquals("Credit", paymentProcessor.processPayment());
Assertions.assertEquals("Online Banking", secondPaymentProcessor.processPayment());
Assertions.assertNull(thirdPaymentProcessor.processPayment());
}
}
7. 依賴注入和構造函數模擬
當我們使用依賴注入時,我們可以直接傳遞mock對象,避免需要mock構造函數。通過這種方法,我們可以在實例化被測試的類之前模擬依賴關係,從而無需模擬任何構造函數。
讓我們在PaymentProcessor
類中引入第三個構造函數,其中PaymentService
作為依賴項注入:
public PaymentProcessor(PaymentService paymentService) {
this.paymentService = paymentService;
}
我們已經將依賴項與PaymentProcessor
類解耦,這使我們能夠單獨測試我們的單元,並通過模擬控制依賴項的行為,如下所示:
@Test
void whenDependencyInjectionIsUsed_ThenMockObjectShouldBeCreated(){
PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
when(mockPaymentService.processPayment()).thenReturn("Online Banking");
PaymentProcessor paymentProcessor = new PaymentProcessor(mockPaymentService);
Assertions.assertEquals("Online Banking", paymentProcessor.processPayment());
}
然而,在我們無法控制源代碼中如何管理依賴項的情況下,特別是當依賴項注入不可行時, mockConstruction()
成為有效模擬構造函數的有用工具。
八、結論
這篇簡短的文章展示了通過 Mockito 和 PowerMock 模擬構造函數的不同方法。我們還討論了在可行的情況下優先考慮依賴注入的優點。
與往常一樣,代碼可以在 GitHub 上獲取。