对时间强依赖的方法如何做单元测试
背景
项目当中需要进行业务时间的校验,如上午 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();
}
}
这样就可以重复执行该单元测试,每次执行的结果应该都是一样的。保持了单元测试的独立性和可重复性。