使用pytest为datetime.now函数设置固定时间

目录

如果函数中使用当前时间,想要使用固定的输入开展测试就需要使用某种方法伪造一个固定时间。 Pytest 提供 monkeypatch 功能支持修改某个对象的属性。

本文介绍如何使用 pytest 让 Python 内置库函数 datetime.datetime.now() 返回某个固定的时间点。

需求

近期仿照 ecFlow 项目为 takler 项目开发时间依赖功能 (TimeAttribute)。 如果某节点设置了时间依赖关系,那么该节点所包含的任务节点只有在指定时刻才会触发运行。

每个工作流都有一个日历属性 (Calendar),保存工作流运行的起始时间 (initial_time) 和工作流的当前时间 (flow_time),该属性由工作流之外的定时任务来更新。 在工作流开始运行时,系统会调用 Calendar.begin() 函数初始化日历属性,该函数使用 datetime.datetime.now() 函数记录工作流启动时的机器时间 initial_real_time。 后续更新日历时间时,会使用该属性计算工作流的当前时间 flow_time

class Calendar:
    ...

    def begin(self, time: datetime.datetime):
        self.initial_time = time
        self.flow_time = time
        self.duration = datetime.timedelta()
        self.increment = datetime.timedelta()
        self.initial_real_time = datetime.datetime.now()
        self.last_real_time = self.initial_real_time

    ...

在测试时,为了得到明确的结果,一般使用确定的输入值传递给测试函数,比如指定一个固定的时间。 initial_real_time 时间取自测试运行时的机器时间,如果测试时间与实际运行的机器时间差别较大,就会产生问题,导致测试失败。 (因为工作流时间通过计算得到,考虑了起始时间与起始机器时间的差值)。 所以需要让 datetime.datetime.now() 返回一个固定的时间,才能完成理想的测试流程。

实现

使用 pytest 的 fixture 和 monkeypatch 功能,从 datetime.datetime 派生一个新类,让 now() 方法返回固定的时间:

import datetime
import pytset


TEST_TIME = datetime.datetime(2022, 9, 12, 10, 0, 0)


@pytest.fixture
def patch_datetime_now(monkeypatch):
    """
    set ``datetime.datetime.now`` to a fixed time, 2022-09-12 10:00:01
    """
    class TestDateTime(datetime.datetime):
        @classmethod
        def now(cls, tz=None):
            return TEST_TIME

    monkeypatch.setattr(datetime, "datetime", TestDateTime)

在测试中启动上述 fixutre patch_datetime_now,测试可以正常通过:

def test_time_attr_catch_time_point(one_task_time_flow, patch_datetime_now):
    flow: Flow = one_task_time_flow.flow
    task1: Task = one_task_time_flow.task1

    start_time = datetime.datetime(2022, 9, 12, 10, 0, 0)
    flow.calendar.begin(start_time)

    flow.update_calendar(datetime.datetime(2022, 9, 12, 11, 0, 0))
    assert not task1.times[0].free
    assert not task1.resolve_time_dependencies()

    flow.update_calendar(datetime.datetime(2022, 9, 12, 12, 0, 0))
    assert task1.times[0].free
    assert task1.resolve_time_dependencies()

    flow.update_calendar(datetime.datetime(2022, 9, 12, 12, 1, 10))
    assert task1.times[0].free

    assert task1.resolve_time_dependencies()

参考

How to monkeypatch python’s datetime.datetime.now with py.test?

How to monkeypatch/mock modules and environments