freezegun是最轻量且兼容性最好的方案,可自动patch datetime.now()、time.time()等标准库时间函数,支持装饰器和上下文管理用法,但不mock monotonic/perf_counter;用于schedule或apscheduler时需配合手动触发调度逻辑;需注意导入方式、第三方时间库、多线程及打包环境等常见陷阱;禁用第三方库时可用unittest.mock.patch精准替换,但路径匹配要求严格。

用 freezegun mock datetime.now() 和 time.time()
绝大多数定时任务依赖 datetime.now()、datetime.utcnow() 或 time.time() 获取当前时间,freezegun 是最轻量且兼容性最好的方案。它会自动 patch 所有标准库中相关函数,包括 datetime 类方法、time 模块、甚至 dateutil 和部分 ORM(如 SQLAlchemy 的 func.now() 在测试环境下也可配合使用)。
安装后直接装饰测试函数或上下文管理:
from freezegun import freeze_time
import datetime
<p>@freeze_time("2024-01-01 12:00:00")
def test_job_runs_at_noon():
assert datetime.datetime.now().hour == 12</p><h1>或在 with 块中控制更细粒度</h1><p>with freeze_time("2024-01-01 09:30:00"):
run_daily_backup() # 此时所有 time/datetime 调用都返回冻结时间</p>
注意:freezegun 默认不 mock time.monotonic() 和 time.perf_counter() —— 这是故意的,因为它们用于测量耗时,mock 反而会干扰性能断言。
当任务用 schedule 或 APScheduler 时,只 mock 时间还不够
这些库内部通常有自己的调度循环和时间判断逻辑(比如每秒检查一次 next_run),单纯冻结系统时间可能让它们“卡住”,因为它们依赖真实流逝来推进调度器状态。
正确做法是:在冻结时间的同时,手动触发调度器的检查逻辑,或改用可控制的运行方式:
-
schedule:调用schedule.run_pending()并配合freeze_time推进到预期时间点 -
APScheduler:停用后台线程(start(paused=True)),用trigger_job()或get_jobs()[0].modify(next_run_time=...)显式控制下次执行时间 - 避免在测试中调用
start()启动真实后台线程 —— 它会脱离控制,导致测试不可靠或超时
绕过 freezegun 的常见陷阱
有些代码会意外绕过 patch,导致 mock 失效:
- 提前导入了
datetime模块并直接用了datetime.datetime.now——freezegunpatch 的是模块对象,不是已绑定的方法。应始终调用datetime.datetime.now()而非存成变量再调用 - 使用了第三方时间工具(如
pendulum.now()、arrow.now())—— 它们不被freezegun自动覆盖,需单独 patch 或改用freezegun.api.freeze_time的扩展机制 - 多线程/协程中未在目标线程内启用 freeze ——
freezegun默认只作用于主线程;若测试涉及子线程,需显式传入ignore_monotonic=False并确认 patch 范围 - 某些打包工具(如 PyInstaller)或容器环境里,
freezegun的 import hook 可能失效,此时应改用freeze_time.start()/.stop()手动生命周期管理
不用第三方库?纯 stdlib 的最小 mock 方案
如果项目禁止引入新依赖,可用 unittest.mock.patch 精准替换关键函数,但必须覆盖所有调用路径:
from unittest.mock import patch
from datetime import datetime
import time
<p>@patch("time.time", return_value=1704081600.0) # 2024-01-01 12:00:00 UTC
@patch("datetime.datetime")
def test_with_stdlib_mock(mock_dt, mock_time):
mock_dt.now.return_value = datetime(2024, 1, 1, 12, 0, 0)
mock_dt.utcnow.return_value = datetime(2024, 1, 1, 12, 0, 0)</p><h1>注意:mock_dt 需要支持 strptime、strftime 等,否则下游解析会崩</h1><pre class="brush:python;toolbar:false;">assert some_task_should_fire_now()
这个方案脆弱点在于:一旦代码里出现 from datetime import datetime 再调用 datetime.now(),patch 就会失效 —— 因为 patch 的是 datetime.datetime,而不是同名的局部导入名。必须确保 patch 目标与实际导入路径完全一致。
真实场景里,定时逻辑常混用多种时间源,最容易漏掉的是日志时间戳、数据库默认值、或配置里的相对时间计算。mock 不全,测试就只是看起来过了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











