對時間強依賴的方法如何做單元測試

背景

項目當中需要進行業務時間的校驗,如上午 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();
    }
}

這樣就可以重複執行該單元測試,每次執行的結果應該都是一樣的。保持了單元測試的獨立性和可重複性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章