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


    背景

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

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

  • 相关阅读:
    找不到msvcr120.dll无法执行代码的全套解决方案
    在线生成发射爱心!生成网站直接发给你的ta
    Sql查询语句
    Linux操作系统~进程替换,exec系列函数的使用
    MindSpore:【模型训练】gpu在训练结束后运行model.时,若打开了混合精度就会报数据类型不兼容
    GreenPlum数据库日常维护
    七夕?程序员不存在的~
    #AcWing 35.反转链表
    基于C++的关键字检索系统
    HTTP协议中URI和URL有什么区别
  • 原文地址:https://www.cnblogs.com/ryuasuka/p/18011027