直接用time.time()或datetime.now()测不出时间敏感逻辑,因为真实系统时间不可控,测试会随运行时刻漂移,且无法可控推进时间验证过期逻辑;根本原因是测试环境未隔离时间源。

为什么直接用time.time()或datetime.now()测不出时间敏感逻辑?
因为真实系统时间不可控,测试用例会随运行时刻漂移——今天通过的测试,明天可能因时区切换、夏令时或跨天逻辑失败。更麻烦的是,你没法“等待30秒”来验证一个过期判断,那会让CI变慢且不稳定。
根本问题不是代码写错了,而是测试环境没隔离时间源。只要逻辑里调用了time.time()、datetime.now()、datetime.utcnow()甚至date.today(),它就依赖外部时钟。
用freezegun冻结时间前必须确认三件事
freezegun本质是 monkey patch,它只对标准库中明确导入的模块生效,不自动覆盖你自己封装的时间工具函数。
- 检查被测代码是否直接调用
datetime.datetime.now(),而不是from datetime import datetime; datetime.now()——后者也能被冻结,但路径必须匹配 - 确认没在测试前手动调用
time.sleep()或依赖threading.Timer等异步时间机制,这些不会被冻结 - 如果项目用了
pendulum、arrow或dateutil,它们默认不受freezegun影响,得额外配置或改用datetime原生对象做断言
pytest中三种最实用的freezegun用法
别一上来就写装饰器。先搞清场景:是单个测试临时冻结?多个测试共用同一时间点?还是整个测试类都需固定上下文?
Python 3.14.2是Python编程语言在2025年12月5日发布的稳定版本,属于3.14系列的第二个维护更新。该版本包含了18项修复,重点解决了多进程、数据类及正则表达式等模块的回归问题,并修复了CVE-2025-12084等安全漏洞。此版本标志着自由线程模式(移除GIL)正式获得官方支持,是Python发展的重要里程碑。
- 单测内临时冻结:
@freeze_time("2023-01-01 12:00:00")<br>def test_token_expires_in_1h():<br> token = generate_token(expire_in=3600)<br> assert token.is_expired() is False - 函数内精确控制(适合需要“推进时间”的场景):
def test_token_becomes_expired_after_1h():<br> with freeze_time("2023-01-01 12:00:00") as frozen:<br> token = generate_token(expire_in=3600)<br> assert not token.is_expired()<br> frozen.tick(delta=3601)<br> assert token.is_expired() - pytest fixture方式(推荐用于多测试复用):
@pytest.fixture<br>def frozen_now():<br> with freeze_time("2023-01-01") as ft:<br> yield ft<br><br>def test_user_age_calculated_from_dob(frozen_now):<br> user = User(dob=date(2000, 1, 1))<br> assert user.age == 23
容易被忽略的兼容性陷阱
freezegun在 Python 3.12+ 对某些 C 扩展时间调用支持还不完善,比如numpy.datetime64或pandas.Timestamp.now()不会被冻结。另外,Django 的timezone.now()需配合freezegun的django插件启用,否则仍走系统时钟。
还有一个隐蔽坑:如果你在conftest.py里全局启用了freeze_time fixture,但某个测试需要真实时间(比如测重试退避),记得显式禁用——@freeze_time(None)或在 fixture 中加条件判断。
真正难的不是冻结时间,而是识别出所有隐式时间依赖点。比如日志里的%(asctime)s、数据库DEFAULT CURRENT_TIMESTAMP、甚至缓存 key 里拼的int(time.time())——这些都不会被freezegun捕获,得靠代码审计补全。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










