背景
項目當中需要進行業務時間的校驗,如上午 9:00-下午 17:00,在 9:00 前或 17:00 後是不能處理相關業務的。因此在業務校驗的 Service
中定義了一個 checkBizTime()
方法。原本代碼如下:
public void checkBizTime() {
Date currentTime = new Date();
// DateUtil.parse的作用是將配置文件中讀取的時間字符串轉換爲Date對象,
// bizStartTimeStr、bizEndTimeStr 是從配置文件中讀取的變量,用 @Value 註解注入
Date bizStartTime = DateUtil.parse(bizStartTimeStr, "HH:mm:ss");
Date bizEndTime = DateUtil.parse(bizEndTimeStr, "HH:mm:ss");
if (currentTime.before(bizStartTime) || currentTime.after(bizEndTime)) {
throw new BizException("不在業務時間範圍內,無法處理業務");
}
}
但是如何對這個方法進行單元測試,成了一個很頭疼的問題。我們知道,單元測試具有獨立性和可重複性,但如果要測試上面這段方法,就會發現當系統時間在 9:00 ~ 17:00 內時,這個方法可以通過測試,而不在這個時間範圍內,這個方法就會拋出異常,也就是說,這個測試方法依賴於當前系統時間,且不同時間運行測試,得到的測試結果是不同的!這違反了單元測試的獨立性和可重複性。因此我們必須讓時間固定在某個特定的時間。
解決方法
解決方法:在 DateUtil
類中建立一個 getCurrentDate()
方法,這個方法返回 new Date()
對象。(如果 DateUtil
是第三方庫的,或是其他人開發的,那麼就在項目中自己定義一個,當然名字需要和 DateUtil
區分開)
public static Date getCurrentDate() {
return new Date();
}
然後把上述業務代碼中的 new Date()
部分替換成 DateUtil.getCurrentDate()
public void checkBizTime() {
Date currentTime = DateUtil.getCurrentDate();
// DateUtil.parse的作用是將配置文件中讀取的時間字符串轉換爲Date對象,
// bizStartTimeStr、bizEndTimeStr 是從配置文件中讀取的變量,用 @Value 註解注入
Date bizStartTime = DateUtil.parse(bizStartTimeStr, "HH:mm:ss");
Date bizEndTime = DateUtil.parse(bizEndTimeStr, "HH:mm:ss");
if (currentTime.before(bizStartTime) || currentTime.after(bizEndTime)) {
throw new BizException("不在業務時間範圍內,無法處理業務");
}
}
然後編寫單元測試,注意要先引入 mockito-inline
這個包,纔可以對靜態方法進行 Mock。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
單元測試代碼如下:
class BizCheckServiceTest {
@InjectMocks
private BizCheckServiceImpl bizCheckServiceUnderTest;
@Mock
private MockedStatic<DateUtil> mockedDateUtil;
@BeforeEach
void setup() {
openMocks(this);
mockedDateUtil
.when(DateUtil::getCurrentDate)
.thenReturn(new Date(2024, 2, 3, 10, 0, 0));
// 假設固定返回 2024年2月3日 10:00:00。但此構造函數已棄用,可以使用其他方式返回Date對象
// 對 DateUtil 類中的其他方法,可以讓他執行真實方法
mockedDateUtil
.when(() -> DateUtil.parse(anyString(), anyString()))
.thenCallRealMethod();
}
@Test
void testCheckBizTime() {
bizCheckServiceUnderTest.checkBizTime();
// 驗證 getCurrentTime() 方法被執行1次,
// parse() 方法被執行2次
verify(mockedDateUtil, times(1)).getCurrentTime();
verify(mockedDateUtil, times(2)).parse(anyString(), anyString());
}
@AfterEach
void tearDown() {
// 每次使用完 MockedStatic 接口需要關閉,不然會導致測試方法報錯
mockedDateUtil.close();
}
}
這樣就可以重複執行該單元測試,每次執行的結果應該都是一樣的。保持了單元測試的獨立性和可重複性。