了解過單元測試相關概念的人應該會清楚一個概念:一個好的單元測試應該是與環境無關的,每一個測試都是相互獨立的。亦即你可以在任何地方,以任意順序運行這些測試,最后得到的結果是一樣的。但是我被測試的類/方法中本身夾雜着對其它類的依賴,這又該怎么處理呢,將依賴進行 mock 是其中一個做法。本文將記錄我在測試過程中的一些備忘,以及遇到的一些問題。
背景說明
我要對我正在開發的一個考試系統中的題目管理部分進行單元測試,這部分主要有一個 SubjectService
接口及其對應的實現類 SubjectServiceImpl
,Service 內部又依賴於 DAO 層的兩個 Mapper(SubjectMapper
和 SubjectAnswerMapper
)。現在我要對 Service 層進行單元測試。此為背景。
過程
首先要確定一個概念:測 Service 層,我們要測它的什么?Service 層對數據庫的訪問是通過 DAO 層進行的。那么對數據庫相關的操作就不適宜放在這里進行測試(對它們的測試應該放在 DAO 層)。Service 層作為主要業務邏輯的載體,對 Service 層的測試應該圍繞流程進行(對於不合法的輸入,應該拋出對應的異常;對於正常的輸入,則流程應該能正常走完,至於數據庫訪問的正確與否,交給 DAO 層的單元測試進行保證)。
確定了這一點之后,接下來就可以開始測試流程了。首先是引入相關的測試框架。由於項目采用了 SpringBoot,我參考了參考資料中的內容,構建起整個測試環境的依賴。
然后就是開始編寫相關的測試類:
@RunWith(MockitoJUnitRunner.class)
public class SubjectServiceImplTest {
private SubjectServiceImpl subjectServiceImpl;
}
對於 Service 所依賴的兩個 DAO,只需要創建對應的兩個 Mapper 並為其加上 @Mock
注解,然后在被測試對象上加上 @InjectMocks
注解,即完成了對依賴的 mock:
@RunWith(MockitoJUnitRunner.class)
public class SubjectServiceImplTest {
@Mock
private SubjectMapper subjectMapper;
@Mock
private SubjectAnswerMapper subjectAnswerMapper;
@InjectMocks
private SubjectServiceImpl subjectServiceImpl;
}
然后就可以開始測試我們的 Service 的方法了。由於 Mock 的引入,現在測試方法的整個流程變成了 4 個步驟:
- 准備測試用的輸入
- 給 Mock 對象設置預期的輸出(因為被測對象所依賴的是由你虛擬出來的東西,所以依賴應該怎么響應需要你手動設置)
- 運行被測方法
- 檢查運行結果是否與預期一致
以下是一個例子
/**
* 測試插入沒有答案的試題
* 應該拋出異常
*/
@Test
public void testSaveSubjectWithoutAnswer() {
SubjectDTO subjectDTO = new SubjectDTO();
subjectDTO.setName("testSubject");
subjectDTO.setDifficulty(1L);
subjectDTO.setCategoryId(1L);
subjectDTO.setSubjectTypeId(1L);
Mockito.when(subjectMapper.insert(Mockito.any()))
.thenReturn(1);
try {
subjectService.saveSubject(subjectDTO);
} catch (BusinessException e) {
assertEquals(e.getCode(), ResultEnum.INCOMPLETE_ADD_EXERCISE_INFORMATION.getCode());
return;
}
throw new RuntimeException("Should not reach here!");
}
在上面的例子中,步驟 2 使用到了 Mockito
類的一些靜態方法,設定了 Mapper 里會被調用方法的響應。(這里建議為了簡化代碼,可以通過 import static
的方式引入 Mockito
的所有方法,這樣可以省略前面的類名)受限於我使用的 JUnit 為 JUnit 4,所以對異常的測試只能這樣進行,在 JUnit 5 中就添加了對預期拋出異常的 assert。
會做這個測試,其余的測試也就基本能夠進行下去了。
遇到的問題
在跑的過程中,我發現了一個挺棘手的問題,目前還沒找到合適的方案。
項目的 DAO 層使用的是 MyBatis + 通用 Mapper 這一套框架。我在 Mock 方法的時候發現在運行的過程中,有關 Mapper 方法中的 selectByExample
的部分總是運行不了,我在方法內部寫了創建 Example
的過程,如果使用 Mock,創建 Example 的過程會出現異常,內容大概是要依賴一個數據庫環境。所以在不考慮 Service 和 DAO 集成測試的情況下,涉及到這部分的 Service 的部分無法進行測試,后續我會繼續查閱相關資料並更新此文。