多層模擬注入 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 上找到。