time-machine 与 freezegun 对比:Python 时间 Mock 怎么选
对照 time-machine 与 freezegun 的 API、时间推进、补丁范围、asyncio 和框架测试差异,说明 time-machine 3.x 的 monotonic 变化;不提供未经复现的性能排名。
本文依据官方文档整理,未执行运行验证或性能基准。代码片段展示局部用法,业务函数、数据和环境需按项目补齐;版本与配置以所引文档为准。
直接回答:time-machine 和 freezegun 都用来在测试中冻结/穿越时间,无需等待真实时钟或修改系统时间。freezegun 成熟、API 直观、社区采用广;time-machine 基于 C 扩展实现,可替换CPython内置时间函数的底层入口,选型需核对支持的解释器、函数与版本。
为什么需要时间 Mock
优惠券过期、定时任务触发、Token 有效期、报表按日聚合——业务里到处是时间依赖。测试这些逻辑如果只能"等真实时间到点",套件就没法跑了。时间 Mock 库让你把 datetime.now() 钉在任意时刻,或快进到未来。
freezegun:老牌易用
from freezegun import freeze_time
@freeze_time("2026-01-01 09:00:00")
def test_coupon_expiry():
coupon = Coupon(expires_in_days=7)
assert not coupon.is_expired()
with freeze_time("2026-01-10 09:00:00"):
assert coupon.is_expired()
装饰器、上下文管理器两种用法都有,API 直觉友好,多年社区积累,可用于Django/Flask等项目的时间逻辑测试。
time-machine:现代高性能
import time_machine
def test_coupon_expiry():
with time_machine.travel("2026-01-01 09:00:00", tick=False) as traveller:
coupon = Coupon(expires_in_days=7)
assert not coupon.is_expired()
traveller.move_to("2026-01-10 09:00:00")
assert coupon.is_expired()
核心实现是 C 扩展,在 C 层面拦截时间调用,其开销特点与Python层遍历模块补丁不同,具体差异需项目基准确认;move_to 可以随时把"当前时间"搬到另一个时刻。
快速对比
| 维度 | freezegun | time-machine |
|---|---|---|
| 实现 | 纯 Python 打补丁 | C 扩展拦截 |
| 性能 | 会扫描模块并替换引用,存在补丁开销 | 替换支持的内置时间函数入口,收益由套件验证 |
| API 风格 | 装饰器 + 上下文管理器,直觉 | 上下文管理器为主 + move_to |
| 补丁覆盖面 | 支持的Python模块引用,有默认参数等盲区 | 支持的CPython函数入口,不覆盖任意C/外部时钟 |
| 成熟度 | 多年社区验证 | 较新但已被广泛采用 |
| Python 版本要求 | 宽松 | 需要较新版本 |
时间冻结与时间旅行
两者都支持两种模式:冻结(时间停在某一时刻不动)和偏移/旅行(时间正常流动但从指定起点开始)。time-machine 的 tick=False 参数控制冻结;freezegun 用 tick=True 开启流动。测试"缓存 TTL 到期"用旅行模式,测试"跨年报表边界"用冻结模式。
打补丁范围的坑
freezegun 在进入冻结上下文时扫描并替换已导入模块中的相关引用,因此并非“提前from-import就必然失效”;默认参数、对象属性或容器持有的引用等仍可能漏过。time-machine替换支持的CPython时间函数实现,不等于拦截系统时钟,任意C扩展自己调用系统时间、数据库服务端时间和子进程不自动受控。
框架集成
两者对 pytest 都友好:freezegun 有 pytest-freezegun 插件提供 freezer fixture;time-machine 可以直接包一层 fixture:
import pytest
@pytest.fixture
def frozen_time():
with time_machine.travel("2026-06-01", tick=False) as t:
yield t
Django项目可按其时区设置配合这些工具;不要假定数据库服务端的 NOW() 也会随Python进程冻结。
性能对比的实践结论
本文未运行对比基准,不提供倍数结论。性能敏感时可用自己的套件验证进入/退出上下文的耗时、总运行时间和边界兼容性;已有freezegun资产且运行稳定时不必只为抽象排名迁移。
高级特性与边界
- 时区:两者都支持 tz-aware 时间,测试跨时区逻辑时务必显式指定
tz_offset或 tzinfo; - monotonic 时钟:当前time-machine 3.x不mock monotonic;freezegun会冻结monotonic/perf_counter,异步测试可用real_asyncio=True让事件循环看到真实monotonic;
- 嵌套使用:都支持嵌套冻结/旅行,内层生效、退出恢复。
常见问题(FAQ)
Q:新项目直接选哪个?
A:先核对解释器和时间函数支持;需要支持的CPython底层入口替换可评估time-machine,需要已有fixture或freezegun特性可保留freezegun。用少量代表性用例验证再决定。
Q:能混用吗?
A:技术上可以但不推荐——两套补丁机制叠加容易出诡异行为。迁移就整项目迁移。
Q:冻结时间后异步代码里的 sleep 会受影响吗?
A:会受所mock的时钟影响。freezegun冻结monotonic可能阻塞asyncio.sleep,需按文档配置real_asyncio=True;tick/move_to也不等同加速真实sleep。当前time-machine 3.x不mock monotonic,但仍应验证使用的异步框架。
官方参考
资料核对日期:2026-09-29。