freezegun通过monkey patch标准库时间函数(如time.time、datetime.now)实现时间冻结,不修改系统时钟;默认不冻结time.monotonic()或第三方库,需手动配置;pytest中推荐用@freeze_time装饰器,支持固定时间点、自动递增和时区安全写法。

freezegun 为什么能“冻住”时间?
因为 freezegun 实际上是通过 monkey patch 多个 Python 时间相关函数(如 time.time、datetime.datetime.now、time.sleep)来实现的。它不修改系统时钟,只让测试代码看到一个固定的时间点或按指定方式推进的时间流。
这意味着:只要被测代码没绕过标准库(比如直接调用 C 库或 os.times()),freezegun 就能生效;但若用了 time.monotonic() 或第三方时间库(如 arrow、pendulum),默认不会被冻结,需手动 patch 或启用对应参数。
在 pytest 中用 @freeze_time 装饰单个测试函数
最常用也最安全的方式。装饰器会自动 setup/teardown,不影响其他测试。
- 安装后直接导入:
from freezegun import freeze_time - 冻结到具体时间点:
@freeze_time("2023-01-01 12:00:00")—— 注意字符串格式必须匹配datetime.strptime默认解析规则,否则抛ValueError - 支持相对偏移:
@freeze_time("2023-01-01 12:00:00", auto_tick_seconds=1),这样每次调用time.sleep(1)后,冻结时间会自动 +1 秒 - 避免用
tz_offset参数设时区,容易出错;推荐传带 tzinfo 的datetime对象,例如@freeze_time(datetime(2023, 1, 1, 12, tzinfo=timezone.utc))
测试涉及 time.sleep 或定时逻辑时的常见陷阱
很多人以为加了 @freeze_time 就能跳过 time.sleep() 等待,其实不然 —— 默认情况下 freezegun 只“冻结”时间读取,不加速执行。真正要跳过等待,得靠 auto_tick_seconds 或显式控制。
-
time.sleep(5)在默认冻结下仍会真实阻塞 5 秒,除非你启用了auto_tick_seconds或使用tick()手动推进 - 想模拟“睡了 5 秒后时间前进 5 秒”,用
@freeze_time("2023-01-01", auto_tick_seconds=1),再调time.sleep(5)即可 - 若逻辑里混用了
time.time()和time.monotonic(),后者不会被冻结,可能导致判断失准(比如超时检测失败) - 异步代码中
asyncio.sleep()不受freezegun影响,得换用pytest-asyncio配合手动 mock 或改用trio/anyio的可冻结时钟
全局冻结时间(慎用)
不推荐在 conftest.py 里用 freeze_time.start() 全局冻结,因为容易污染后续测试、难以调试、且和 pytest 的 fixture 生命周期冲突。
- 如果真需要跨多个测试共享同一时间上下文,优先用 pytest fixture 封装
freeze_time实例,并明确声明scope="function"或"module" - 手动 start/stop 容易漏掉 teardown,导致后续测试时间异常,出现“某次运行通过、下次失败”的诡异问题
- 某些 CI 环境(如 GitHub Actions)对 monkey patch 敏感,全局冻结可能引发不可预知的 subprocess 行为变化
真正难处理的是那些隐式依赖系统时钟的底层行为,比如日志轮转、数据库连接池空闲超时、JWT token 过期校验 —— 这些地方 freeze_time 能起效,但必须确认所有路径都走的是标准时间接口,而不是绕过 Python 层的系统调用。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











