多層模擬注入 Mockito 間諜對象
1. 概述
在本教程中,我們將討論著名的 Mockito 註解@InjectMock 、 @Mock 、 @Spy並了解它們如何在多層注入場景中協同工作。我們將討論重要的測試概念並學習如何進行正確的測試配置。
2. 多層注入概念
多層次注入是一個強大的概念,但誤用時可能會很危險。在繼續實施之前,讓我們回顧一下重要的理論概念。
2.1.單元測試概念
根據定義,單元測試是覆蓋一個原始碼單元的測試。在 Java 世界中,我們可以將單元測試視為涵蓋某些特定類別(服務、儲存庫、實用程式等)的測試。
當測試一個類別時,我們只想測試它的業務邏輯,而不是它的依賴項的行為。為了處理依賴關係,例如模擬它們,或驗證它們的使用情況,我們通常使用模擬框架 – Mockito。它旨在擴展現有的測試引擎(JUnit、TestNG),並幫助正確地為具有多個相依性的類別建置單元測試。
2.2. @ Spy概念
Spy是 Mockito 的重要支柱之一,有助於有效地處理依賴項。
Mock是一個完整的存根,在呼叫方法時不執行任何操作,並且不會到達實際物件。相反, Spy預設將所有呼叫委託給真實物件方法。當指定時,間諜方法可以表現為具有其所有功能的模擬。
由於間諜物件作為真實物件的預設行為,我們需要設定必要的依賴項。 Mockito 嘗試將依賴項隱式註入到間諜物件中。但是,我們可以在需要時明確設定依賴關係。
2.3.多層次注入風險
Mockito中的多層注入是指被測試的類別需要一個spy的場景,而這個spy依賴於特定的mock進行注入,創建一個嵌套的注入結構。
在某些場景中,我們可能需要進行多層模擬。例如,我們正在測試一些ServiceA ,它依賴一些複雜的MapperA ,而MapperA又依賴不同的ServiceB 。一般來說,讓MapperA成為間諜並注入ServiceB作為模擬會更容易。然而,這種方法打破了單元測試的概念。當我們需要在測試中覆蓋多個服務時,我們應該堅持完成整合測試。
如果我們經常面臨多層注入的需求,這可能是測試方法不正確或程式碼設計複雜的跡象,應該進行重構。
3. 設定範例場景
在繼續測試用例之前,讓我們先定義範例程式碼來展示如何使用 Mockito 來測試我們的程式碼庫。
我們將使用圖書館概念,其中我們有一個主要的Book實體,該實體正在由多個服務處理。這裡最重要的一點是類別之間的依賴關係。
處理的入口點是BookStorageService ,旨在儲存有關獲取/給予書籍的資訊並透過BookControlService驗證書籍狀態:
public class BookStorageService {
private BookControlService bookControlService;
private final List<Book> availableBooks;
}
另一方面, BookControlService依賴另外兩個類, StatisticService和RepairService ,它們應該計算處理的書籍數量並檢查書籍是否應該修復:
public class BookControlService {
private StatisticService statisticService;
private RepairService repairService;
}
4. 深入使用@InjectMock註解
當我們考慮將一個 Mockito 管理的類別注入另一個類別時, @InjectMock註解看起來是最直觀的機制。然而,它的能力是有限的。該文件強調 Mockito 不應被視為依賴注入框架。它並不是為處理物件網路的複雜注入而設計的。
此外,Mockito 不會報告任何注入失敗。換句話說,當 Mockito 無法將模擬注入該欄位時,該欄位將保持為空。因此,如果測試類別設定不正確,我們最終會得到多個NullPointerException (NPE) 。
@InjectMock在不同的配置中表現不同,並不是每個設定都能以所需的方式運作。讓我們詳細回顧一下註釋使用的特點。
4.1. @InjectMock和Spy不工作配置
在一個類別中使用多個@InjectMock註解可能很直觀:
public class MultipleInjectMockDoestWorkTest {
@InjectMocks
private BookStorageService bookStorageService;
@Spy
@InjectMocks
private BookControlService bookControlService;
@Mock
private StatisticService statisticService;
}
此類配置的目的是將statisticService模擬注入bookControlService間諜中,並將bookContorlService注入bookStorageService 。然而,這樣的配置不起作用,並且會在較新的 Mockito 版本中導致 NPE。
在底層,目前版本的框架(5.10.0)將所有註釋的物件收集到兩個集合中。第一個集合用於使用@InjectMock註解的模擬相關欄位( bookStorageService和bookControlService )。
第二個集合是針對所有候選注入的實體,它們都是模擬物件和合格的間諜。但是,同時標記為 @Spy 和 @InjectMock 的欄位Spy @InjectMock被視為注入的候選者。因此,Mockito 不會知道應該將bookControlService注入到bookStorageService中。
上述配置的另一個問題是概念性的。使用@InjectMock註釋,我們的目標是在類別中測試兩個類別( BookStorageService和BookControlService ),這違反了單元測試方法。
4.2. @InjectMock和@Spy工作配置
同時, @Spy和@InjectMock一起使用沒有限制,只要類別中只有一個@InjectMock註解即可:
@Spy
@InjectMocks
private BookControlService bookControlService;
@Mock
private StatisticService statisticService;
@Spy
private RepairService repairService;
透過此配置,我們擁有正確建置且可測試的層次結構:
@Test
void whenOneInjectMockWithSpy_thenHierarchySuccessfullyInitialized(){
Book book = new Book("Some name", "Some author", 355, ZonedDateTime.now());
bookControlService.returnBook(book);
Assertions.assertNull(book.getReturnDate());
Mockito.verify(statisticService).calculateAdded();
Mockito.verify(repairService).shouldRepair(book);
}
5.透過@InjectMock和手動Spy進行多層注入
解決多層注入的選項之一是在 Mockito 初始化之前手動實例化間諜物件。正如我們已經討論過的,Mockito 無法將所有依賴項注入到該字段中,該字段同時使用@Spy和@InjectMock.但是,框架可以將依賴項注入到僅用@InjectMock註釋的對象,即使該對像是間諜。
我們可以在類別層級使用@ExtendWith(MockitoExtension.class)並在欄位中初始化間諜:
@InjectMocks
private BookControlService bookControlService = Mockito.spy(BookControlService.class);
或者我們可以使用MockitoAnnotations.openMocks(this)並在@BeforeEach方法中初始化間諜:
@BeforeEach
public void openMocks() {
bookControlService = Mockito.spy(BookControlService.class);
closeable = MockitoAnnotations.openMocks(this);
}
在這兩種情況下,都應該在 Mockito 初始化之前創建間諜。
透過上述設置,Mockito 在手動創建的間諜上處理@InjectMock並注入所有需要的模擬:
@InjectMocks
private BookStorageService bookStorageService;
@InjectMocks
private BookControlService bookControlService;
@Mock
private StatisticService statisticService;
@Mock
private RepairService repairService;
測試執行成功:
@Test
void whenSpyIsManuallyCreated_thenInjectMocksWorks() {
Book book = new Book("Some name", "Some author", 355);
bookStorageService.returnBook(book);
Assertions.assertEquals(1, bookStorageService.getAvailableBooks().size());
Mockito.verify(bookControlService).returnBook(book);
Mockito.verify(statisticService).calculateAdded();
Mockito.verify(repairService).shouldRepair(book);
}
6. 透過反射進行多層次注入
處理複雜測試設定的另一種可能方法是手動建立所需的間諜對象,然後將其註入到被測對像中。利用反射機制,我們可以使用所需的間諜來更新 Mockito 創建的物件。
在下面的範例中,我們沒有使用InjectMocks註解BookControlService ,而是手動配置了所有內容。為了確保 Mockito 創建的模擬在間諜初始化期間可用,必須先初始化 Mockito 上下文。否則,無論何時使用模擬,都可能會發生NullPointerException 。
一旦BookControlService間諜配置了所有模擬,我們就透過反射將其註入BookStorageService :
@InjectMocks
private BookStorageService bookStorageService;
@Mock
private StatisticService statisticService;
@Mock
private RepairService repairService;
private BookControlService bookControlService;
@BeforeEach
public void openMocks() throws Exception {
bookControlService = Mockito.spy(new BookControlService(statisticService, repairService));
injectSpyToTestedMock(bookStorageService, bookControlService);
}
透過這樣的配置,我們可以驗證repairService和bookControlService的行為。
七、結論
在本文中,我們回顧了重要的單元測試概念,並學習如何使用InjectMock 、 @Spy和Mock註釋來執行複雜的多層注入。我們已經發現瞭如何手動配置間諜以及如何將其註入到測試對像中。
與往常一樣,完整的範例可以在 GitHub 上找到。