对时间强依赖的方法如何做单元测试

背景

项目当中需要进行业务时间的校验,如上午 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();
    }
}

这样就可以重复执行该单元测试,每次执行的结果应该都是一样的。保持了单元测试的独立性和可重复性。

热门相关:秾李夭桃   与校花同居:高手风流   佣兵之王都市行   神医嫁到   我和超级大佬隐婚了