你可能没有听说过的有用的Python库:Freezegun
在你的Python测试中让时间停滞。

AI(GPT-4o)制作的图像
我认为我们都可以同意,测试我们的代码是软件开发生命周期中至关重大和必不可少的一部分。当我们讨论人工智能和机器学习系统时,这可能更加重大,由于这些系统从一开始就可能具有固有的不确定性和幻觉元素。
在这个通用的测试框架中,测试根据当前日期或时间表现不同的代码可能会让人头疼。如何可靠地检查仅在午夜触发的逻辑,计算相对日期(“2小时前”),或处理像闰年或月底这样棘手的情况?手动模拟Python的datetime模块可能会很麻烦且容易出错。
如果你曾经为此而苦恼过,你并不孤单。但如果你能简单地…停止时间呢?甚至在你的测试中穿越时间呢?
这正是Freezegun库让你能够做到的。这是一个对于常见测试问题的优雅解决方案,不过许多经验丰富的Python开发人员从未听说过它。
Freezegun允许您的Python测试通过模拟datetime、date、time和pendulum Python模块中的特定时刻来模拟时间。它易于使用,但对于创建确定性和可靠的时间敏感代码测试超级强劲。
为什么Freezegun如此有用?
1. 决定论。这是Freezegun的主要优点。涉及时间的测试变得完全可预测。在冻结块中运行datetime.now()会返回一样的冻结时间戳,消除了由毫秒差异或测试执行期间日期翻转引起的不稳定测试。
2. 与手动修补datetime.now或使用unittest.mock相比,Freezegun一般更简洁,需要更少的样板代码,特别是在临时更改时间时。
3. 时间旅行。轻松模拟特定日期和时间 – 过去、目前或未来。这对于测试边缘情况超级重大,列如年末处理、闰秒、夏令时转换,或者仅仅是验证与特定事件相关的逻辑。
4. 相对时间测试。通过冻结时间并创建相对于该冻结时刻的时间戳来测试计算相对时间(例如“3天后过期”)的函数。
5. 滴答滴答。Freezegun允许时间从测试中的冻结时刻向前推进(“滴答”),超级适合测试超时、持续时间或时间相关事件序列。
希望我已经说服了你,Freezegun可能是你的Python工具箱中有价值的补充。让我们通过查看一些示例代码片段来看它的运行情况。
搭建开发环境
但在那之前,让我们建立一个开发环境来进行实验。我使用Miniconda,但你可以使用任何你熟悉的工具。
我是一个Windows用户,但我常常使用WSL2 Ubuntu for Windows进行开发,这也是我在这里要做的事情。
我展示的所有代码都应该在Windows或类Unix操作系统下同样有效。
# 创建并激活一个新的开发环境
#
(base) $ conda create -n freezegun python=3.12 -y
(base) $ conda activate freezegun
# 创建并激活一个新的开发环境
#
(base) $ conda create -n freezegun python=3.12 -y
(base) $ conda activate freezegun
目前,我们可以安装剩余的必要库。
(freezegun) $ pip install freezegun jupyter
(freezegun)$ pip安装freezegun jupyter
(freezegun) $ pip install freezegun jupyter
我将使用Jupyter Notebook来运行我的代码。要跟着操作,请在命令提示符中输入jupyter notebook。您应该会在浏览器中看到一个Jupyter Notebook打开。如果这不会自动发生,您可能会在输入jupyter notebook命令后看到一屏幕的信息。在底部附近,您会找到一个URL,将其复制并粘贴到浏览器中以启动Jupyter Notebook。
Jupyter笔记本
Jupyter笔记本
您的URL将与我的不同,但它应该看起来类似于这样:-
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
一个小插曲:在下面的示例中,我展示的代码大量使用了Python的assert命令。如果你之前没有遇到过这个函数或者在Python中没有做过太多单元测试,assert用于测试条件是否为真,如果条件不成立,它会引发一个AssertionError。这有助于在开发过程中捕获问题,并常用于调试和验证代码中的假设。
一个快速的提示:我在下面的示例中展示的代码大量使用了Python的assert命令。如果你以前没有遇到过这个函数,或者在Python中没有进行过太多的单元测试,assert用于测试条件是否为真,如果不是,则会引发AssertionError。这有助于在开发过程中捕获问题,一般用于调试和验证代码中的假设。
断言错误。
示例1:使用装饰器实现基本的时间冻结
使用Freezegun最常见的方式是通过其装饰器@freeze_time,它允许您“设置”一天中的特定时间来测试各种与时间相关的函数。
“`python
import datetime
from freezegun import freeze_time
def get_greeting():
now = datetime.datetime.now()
print(f” Inside get_greeting(), now = {now}”) # Added print
if now.hour < 12:
return “早上好!”
elif 12 <= now.hour < 18:
return “下午好!”
else:
return “晚上好!”
# 测试早上的问候
@freeze_time(“2023-10-27 09:00:00”)
def test_morning_greeting():
print(“运行测试 test_morning_greeting:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
assert greeting == “早上好!”
# 测试晚上的问候
@freeze_time(“2023-10-27 21:30:00”)
def test_evening_greeting():
print(”
运行测试 test_evening_greeting:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
assert greeting == “晚上好!”
# 运行测试
test_morning_greeting()
test_evening_greeting()
print(”
基本装饰器测试通过!”)
# — 失败场景 —
# 如果我们不冻结时间会发生什么?
print(”
— 运行未冻结时间的情况(根据实际时间可能会失败) —“)
def
test_morning_greeting_unfrozen():
print(“运行测试
test_morning_greeting_unfrozen:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
# 这个断言目前不可靠!它取决于你运行代码的时间。
try:
assert greeting == “早上好!”
print(” (偶然通过)”)
except AssertionError:
print(” (预期失败 – 时间不是早上9点)”)
test_morning_greeting_unfrozen()
“`
“`python
import datetime
from freezegun import freeze_time
def get_greeting():
now = datetime.datetime.now()
print(f” Inside get_greeting(), now = {now}”) # Added print
if now.hour < 12:
return “早上好!”
elif 12 <= now.hour < 18:
return “下午好!”
else:
return “晚上好!”
# 测试早上的问候
@freeze_time(“2023-10-27 09:00:00”)
def test_morning_greeting():
print(“运行测试早上的问候:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
assert greeting == “早上好!”
# 测试晚上的问候
@freeze_time(“2023-10-27 21:30:00”)
def test_evening_greeting():
print(”
运行测试晚上的问候:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
assert greeting == “晚上好!”
# 运行测试
test_morning_greeting()
test_evening_greeting()
print(”
基本装饰器测试通过!”)
# — 失败场景 —
# 如果我们不冻结时间会发生什么?
print(”
— 不使用 freeze_time 运行(根据实际时间可能失败) —“)
def
test_morning_greeting_unfrozen():
print(“运行未冻结时间的测试早上的问候:”)
greeting = get_greeting()
print(f” -> 得到的问候: '{greeting}'”)
# 这个断言目前不可靠!它取决于代码运行的时间。
try:
assert greeting == “早上好!”
print(” (偶然通过)”)
except AssertionError:
print(” (如预期的那样失败 – 时间不是上午9点)”)
test_morning_greeting_unfrozen()
“`
输出。
运行test_morning_greeting:
在get_greeting()内,当前时间 = 2023年10月27日 09:00:00
-> 收到问候语:'早上好!'
运行test_evening_greeting:
在get_greeting()内,当前时间 = 2023年10月27日 21:30:00
-> 收到问候语:'晚上好!'
基本装饰器测试通过!
— 在不使用freeze_time的情况下运行(根据实际时间可能会失败)—
运行
test_morning_greeting_unfrozen:
在get_greeting()内,当前时间 = 2025年04月16日 15:00:37.363367
-> 收到问候语:'下午好!'
(如预期一样失败-时间不是上午9点)
运行test_morning_greeting:
在get_greeting()内,当前时间为2023年10月27日09:00:00
-> 收到问候语:'早上好!'
运行test_evening_greeting:
在get_greeting()内,当前时间为2023年10月27日21:30:00
-> 收到问候语:'晚上好!'
基本装饰器测试通过!
— 在没有冻结时间的情况下运行(根据实际时间可能会失败)—
运行
test_morning_greeting_unfrozen:
在get_greeting()内,当前时间为2025年04月16日15:00:37.363367
-> 收到问候语:'下午好!'
(如预期一样失败 – 时间不是上午9点)
示例2:使用上下文管理器进行基本时间冻结
创造一个“冻结时间”的“块”。
“`python
import datetime
from freezegun import freeze_time
def process_batch_job():
start_time = datetime.datetime.now()
# 模拟工作
end_time = datetime.datetime.now() # 实际情况下,时间会流逝
print(f” 作业内部:开始时间={start_time},结束时间={end_time}”) # 添加打印
return (start_time, end_time)
def
test_job_timestamps_within_frozen_block():
print(”
运行
test_job_timestamps_within_frozen_block:”)
frozen_time_str = “2023-11-15 10:00:00”
with freeze_time(frozen_time_str):
print(f” 进入冻结块时间:{frozen_time_str}”)
start, end = process_batch_job()
print(f” 断言开始时间 == 结束时间:{start} == {end}”)
assert start == end
print(f” 断言开始时间 == 冻结时间:{start} == {datetime.datetime(2023, 11, 15, 10, 0, 0)}”)
assert start == datetime.datetime(2023, 11, 15, 10, 0, 0)
print(” 冻结块内的断言通过。”)
print(” 退出冻结块。”)
now_outside = datetime.datetime.now()
print(f” 冻结块外的时间:{now_outside}(应为实时时间)”)
# 此断言只是显示时间已解冻,值取决于实时时间
assert now_outside != datetime.datetime(2023, 11, 15, 10, 0, 0)
test_job_timestamps_within_frozen_block()
print(”
上下文管理器测试通过!”)
“`
“`python
import datetime
from freezegun import freeze_time
def process_batch_job():
start_time = datetime.datetime.now()
# 模拟工作
end_time = datetime.datetime.now() # 实际情况下,时间会流逝
print(f” 作业内部:开始时间={start_time},结束时间={end_time}”) # 添加打印
return (start_time, end_time)
def
test_job_timestamps_within_frozen_block():
print(”
运行
test_job_timestamps_within_frozen_block:”)
frozen_time_str = “2023-11-15 10:00:00”
with freeze_time(frozen_time_str):
print(f” 进入冻结块时间为 {frozen_time_str}”)
start, end = process_batch_job()
print(f” 断言开始时间 == 结束时间:{start} == {end}”)
assert start == end
print(f” 断言开始时间 == 冻结时间:{start} == {datetime.datetime(2023, 11, 15, 10, 0, 0)}”)
assert start == datetime.datetime(2023, 11, 15, 10, 0, 0)
print(” 冻结块内的断言通过。”)
print(” 退出冻结块。”)
now_outside = datetime.datetime.now()
print(f” 冻结块外的时间:{now_outside}(应为真实时间)”)
# 此断言只是显示时间未冻结,值取决于真实时间
assert now_outside != datetime.datetime(2023, 11, 15, 10, 0, 0)
test_job_timestamps_within_frozen_block()
print(”
上下文管理器测试通过!”)
“`
输出。
运行
test_job_timestamps_within_frozen_block:
进入冻结块时间为2023-11-15 10:00:00
作业内部:开始时间=2023-11-15 10:00:00,结束时间=2023-11-15 10:00:00
断言开始时间等于结束时间:2023-11-15 10:00:00 == 2023-11-15 10:00:00
断言开始时间等于冻结时间:2023-11-15 10:00:00 == 2023-11-15 10:00:00
块内断言通过。
退出冻结块。
块外时间:2025-04-16 15:10:15.231632(应为实时时间)
上下文管理器测试通过!
运行
test_job_timestamps_within_frozen_block:
进入冻结块时间为2023-11-15 10:00:00
在作业内部:开始时间=2023-11-15 10:00:00,结束时间=2023-11-15 10:00:00
断言开始时间等于结束时间:2023-11-15 10:00:00 == 2023-11-15 10:00:00
断言开始时间等于冻结时间:2023-11-15 10:00:00 == 2023-11-15 10:00:00
块内的断言通过。
退出冻结块。
块外的时间:2025-04-16 15:10:15.231632(应为真实时间)
上下文管理器测试通过!
示例3:使用tick推进时间
在冻结期间模拟时间流逝。
“`python
import datetime
import time
from freezegun import freeze_time
def check_if_event_expired(event_timestamp, expiry_duration_seconds):
now = datetime.datetime.now()
expired = now > event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)
print(f” Checking expiry: Now={now}, Event={event_timestamp}, ExpiresAt={event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)} -> Expired={expired}”)
return expired
# — 使用上下文管理器手动进行时间推移 —
def
test_event_expiry_manual_tick():
print(”
Running
test_event_expiry_manual_tick:”)
with freeze_time(“2023-10-27 12:00:00”) as freezer:
event_time_in_freeze = datetime.datetime.now()
expiry_duration = 60
print(f” Event created at: {event_time_in_freeze}”)
print(” Checking immediately after creation:”)
assert not check_if_event_expired(event_time_in_freeze, expiry_duration)
# 推进时间 61 秒
delta_to_tick = datetime.timedelta(seconds=61)
print(f” Ticking forward by {delta_to_tick}…”)
freezer.tick(delta=delta_to_tick)
print(f” Time after ticking: {datetime.datetime.now()}”)
print(” Checking after ticking:”)
assert check_if_event_expired(event_time_in_freeze, expiry_duration)
print(” Manual tick test finished.”)
# — 失败场景 —
@freeze_time(“2023-10-27 12:00:00”) # 没有 tick=True 或手动推进时间
def
test_event_expiry_fail_without_tick():
print(”
— Running
test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) —“)
event_time = datetime.datetime.now()
expiry_duration = 60
print(f” Event created at: {event_time}”)
# 模拟工作或等待 – 没有推进时间,时间不会前进!
time.sleep(0.1)
print(f” Time after simulated wait: {datetime.datetime.now()}”)
print(” Checking expiry (incorrectly, time didn't move):”)
try:
# 理想情况下应该为 True,但没有推进时间则为 False
assert check_if_event_expired(event_time, expiry_duration)
except AssertionError:
print(” AssertionError: Event did not expire, as expected without tick.”)
print(” Failure scenario finished.”)
# 运行两个测试
test_event_expiry_manual_tick()
test_event_expiry_fail_without_tick()
“`
“`python
import datetime
import time
from freezegun import freeze_time
def check_if_event_expired(event_timestamp, expiry_duration_seconds):
now = datetime.datetime.now()
expired = now > event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)
print(f” Checking expiry: Now={now}, Event={event_timestamp}, ExpiresAt={event_timestamp + datetime.timedelta(seconds=expiry_duration_seconds)} -> Expired={expired}”)
return expired
# — 使用上下文管理器手动进行时钟推进 —
def
test_event_expiry_manual_tick():
print(”
Running
test_event_expiry_manual_tick:”)
with freeze_time(“2023-10-27 12:00:00”) as freezer:
event_time_in_freeze = datetime.datetime.now()
expiry_duration = 60
print(f” Event created at: {event_time_in_freeze}”)
print(” Checking immediately after creation:”)
assert not check_if_event_expired(event_time_in_freeze, expiry_duration)
# 推进时间 61 秒
delta_to_tick = datetime.timedelta(seconds=61)
print(f” Ticking forward by {delta_to_tick}…”)
freezer.tick(delta=delta_to_tick)
print(f” Time after ticking: {datetime.datetime.now()}”)
print(” Checking after ticking:”)
assert check_if_event_expired(event_time_in_freeze, expiry_duration)
print(” Manual tick test finished.”)
# — 失败场景 —
@freeze_time(“2023-10-27 12:00:00”) # 没有 tick=True 或手动推进
def
test_event_expiry_fail_without_tick():
print(”
— Running
test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) —“)
event_time = datetime.datetime.now()
expiry_duration = 60
print(f” Event created at: {event_time}”)
# 模拟工作或等待 – 没有推进,时间不会前进!
time.sleep(0.1)
print(f” Time after simulated wait: {datetime.datetime.now()}”)
print(” Checking expiry (incorrectly, time didn't move):”)
try:
# 理想情况下应为 True,但没有推进时会为 False
assert check_if_event_expired(event_time, expiry_duration)
except AssertionError:
print(” AssertionError: Event did not expire, as expected without tick.”)
print(” Failure scenario finished.”)
# 运行两个测试
test_event_expiry_manual_tick()
test_event_expiry_fail_without_tick()
“`
这将输出以下内容。
运行
test_event_expiry_manual_tick测试:
事件创建于:2023年10月27日 12:00:00
创建后立即检查:
检查到期时间:当前时间=2023年10月27日 12:00:00,事件时间=2023年10月27日 12:00:00,到期时间=2023年10月27日 12:01:00 -> 已过期=False
向前推进 0:01:01…
推进后的时间:2023年10月27日 12:01:01
推进后检查:
检查到期时间:当前时间=2023年10月27日 12:01:01,事件时间=2023年10月27日 12:00:00,到期时间=2023年10月27日 12:01:00 -> 已过期=True
手动推进测试完成。
— 运行
test_event_expiry_fail_without_tick测试(期望断言错误) —
事件创建于:2023年10月27日 12:00:00
模拟等待后的时间:2023年10月27日 12:00:00
检查到期时间(错误地,时间未移动):
检查到期时间:当前时间=2023年10月27日 12:00:00,事件时间=2023年10月27日 12:00:00,到期时间=2023年10月27日 12:01:00 -> 已过期=False
断言错误:事件未按预期在未推进时过期。
失败场景完成。
运行
test_event_expiry_manual_tick:
创建事件时间:2023-10-27 12:00:00
创建后立即检查:
检查到期时间:当前时间=2023-10-27 12:00:00,事件时间=2023-10-27 12:00:00,到期时间=2023-10-27 12:01:00 -> 已过期=False
向前推进 0:01:01…
推进后的时间:2023-10-27 12:01:01
推进后检查:
检查到期时间:当前时间=2023-10-27 12:01:01,事件时间=2023-10-27 12:00:00,到期时间=2023-10-27 12:01:00 -> 已过期=True
手动推进测试完成。
— 运行
test_event_expiry_fail_without_tick(预期断言错误)—
创建事件时间:2023-10-27 12:00:00
模拟等待后的时间:2023-10-27 12:00:00
检查到期时间(错误,时间未移动):
检查到期时间:当前时间=2023-10-27 12:00:00,事件时间=2023-10-27 12:00:00,到期时间=2023-10-27 12:01:00 -> 已过期=False
AssertionError: 事件未过期,如预期未推进。
失败场景完成。
示例4:测试相对日期
Freezegun确保了稳定的“时间过去”逻辑。
“`python
import datetime
from freezegun import freeze_time
def format_relative_time(timestamp):
now = datetime.datetime.now()
delta = now – timestamp
rel_time_str = “”
if delta.days > 0:
rel_time_str = f”{delta.days} 天前”
elif delta.seconds >= 3600:
hours = delta.seconds // 3600
rel_time_str = f”{hours} 小时前”
elif delta.seconds >= 60:
minutes = delta.seconds // 60
rel_time_str = f”{minutes} 分钟前”
else:
rel_time_str = “刚刚”
print(f” 格式化相对时间:目前={now},时间戳={timestamp} -> '{rel_time_str}'”)
return rel_time_str
@freeze_time(“2023-10-27 15:00:00”)
def
test_relative_time_formatting():
print(”
运行测试相对时间格式化:”)
# 事件发生在冻结时间相对于2天3小时前
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
assert format_relative_time(past_event) == “2 天前”
# 事件发生在45分钟前
recent_event = datetime.datetime.now() – datetime.timedelta(minutes=45)
assert format_relative_time(recent_event) == “45 分钟前”
# 事件刚刚发生
current_event = datetime.datetime.now() – datetime.timedelta(seconds=10)
assert format_relative_time(current_event) == “刚刚”
print(” 相对时间测试通过!”)
test_relative_time_formatting()
# — 失败场景 —
print(”
— 运行未冻结时间的相对时间(预期失败)—“)
def
test_relative_time_unfrozen():
# 使用一样的过去事件时间戳
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
print(f” 使用 past_event = {past_event} 进行测试”)
# 这将与*实际*当前时间进行比较,而不是2023年10月27日
formatted_time = format_relative_time(past_event)
try:
assert formatted_time == “2 天前”
except AssertionError:
# 实际差异将更大!
print(f” AssertionError: 预期为 '2 天前',但得到 '{formatted_time}'。预期失败。”)
test_relative_time_unfrozen()
“`
“`python
import datetime
from freezegun import freeze_time
def format_relative_time(timestamp):
now = datetime.datetime.now()
delta = now – timestamp
rel_time_str = “”
if delta.days > 0:
rel_time_str = f”{delta.days} 天前”
elif delta.seconds >= 3600:
hours = delta.seconds // 3600
rel_time_str = f”{hours} 小时前”
elif delta.seconds >= 60:
minutes = delta.seconds // 60
rel_time_str = f”{minutes} 分钟前”
else:
rel_time_str = “刚刚”
print(f” 格式化相对时间:目前={now},时间戳={timestamp} -> '{rel_time_str}'”)
return rel_time_str
@freeze_time(“2023-10-27 15:00:00”)
def
test_relative_time_formatting():
print(”
运行测试相对时间格式化:”)
# 事件发生在冻结时间相对于 2 天 3 小时前
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
assert format_relative_time(past_event) == “2 天前”
# 事件发生在 45 分钟前
recent_event = datetime.datetime.now() – datetime.timedelta(minutes=45)
assert format_relative_time(recent_event) == “45 分钟前”
# 事件刚刚发生
current_event = datetime.datetime.now() – datetime.timedelta(seconds=10)
assert format_relative_time(current_event) == “刚刚”
print(” 相对时间测试通过!”)
test_relative_time_formatting()
# — 失败场景 —
print(”
— 运行未冻结时间的相对时间(期望失败)—“)
def
test_relative_time_unfrozen():
# 使用一样的过去事件时间戳
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
print(f” 使用 past_event = {past_event} 进行测试”)
# 这将与*实际*当前时间进行比较,而不是 2023 年 10 月 27 日
formatted_time = format_relative_time(past_event)
try:
assert formatted_time == “2 天前”
except AssertionError:
# 实际差异将更大!
print(f” AssertionError: 期望为 '2 天前',但得到 '{formatted_time}'。如预期失败。”)
test_relative_time_unfrozen()
“`
运行
test_relative_time_formatting:
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-25 12:00:00 -> '2天前'
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-27 14:15:00 -> '45分钟前'
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-27 14:59:50 -> '刚刚'
相对时间测试通过!
— 运行不使用freeze_time的相对时间(期望失败)—
测试过去事件=2023-10-25 12:00:00
格式化相对时间:目前=2023-10-27 12:00:00,时间戳=2023-10-25 12:00:00 -> '2天前'
运行
test_relative_time_formatting:
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-25 12:00:00 -> '2天前'
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-27 14:15:00 -> '45分钟前'
格式化相对时间:目前=2023-10-27 15:00:00,时间戳=2023-10-27 14:59:50 -> '刚刚'
相对时间测试通过!
— 运行不使用freeze_time的相对时间(期望失败)—
测试过去事件=2023-10-25 12:00:00
格式化相对时间:目前=2023-10-27 12:00:00,时间戳=2023-10-25 12:00:00 -> '2天前'
示例5:处理特定日期(月底)
可靠地测试边缘情况,列如闰年。
导入日期时间
从freezegun中导入freeze_time
def is_last_day_of_month(check_date):
next_day = check_date + datetime.timedelta(days=1)
is_last = next_day.month != check_date.month
print(f” 检查{check_date}是否是月底: 下一天={next_day}, 是否是最后一天={is_last}”)
return is_last
print(”
运行特定日期逻辑测试:”)
@freeze_time(“2023-02-28”) # 非闰年
def
test_end_of_february_non_leap():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time(“2024-02-28”) # 闰年
def
test_end_of_february_leap_not_yet():
today = datetime.date.today()
assert is_last_day_of_month(today) is False # 2月29日存在
@freeze_time(“2024-02-29”) # 闰年 – 最后一天
def
test_end_of_february_leap_actual():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time(“2023-12-31”)
def test_end_of_year():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
test_end_of_february_non_leap()
test_end_of_february_leap_not_yet()
test_end_of_february_leap_actual()
test_end_of_year()
print(“特定日期逻辑测试通过!”)
导入日期时间
从freezegun中导入freeze_time
def is_last_day_of_month(check_date):
next_day = check_date + datetime.timedelta(days=1)
is_last = next_day.month != check_date.month
print(f” 检查{check_date}是否是月末:下一天={next_day},是否是最后一天={is_last}”)
return is_last
print(”
运行特定日期逻辑测试:”)
@freeze_time(“2023-02-28”) # 非闰年
def
test_end_of_february_non_leap():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time(“2024-02-28”) # 闰年
def
test_end_of_february_leap_not_yet():
today = datetime.date.today()
assert is_last_day_of_month(today) is False # 2月29日存在
@freeze_time(“2024-02-29”) # 闰年 – 最后一天
def
test_end_of_february_leap_actual():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
@freeze_time(“2023-12-31”)
def test_end_of_year():
today = datetime.date.today()
assert is_last_day_of_month(today) is True
test_end_of_february_non_leap()
test_end_of_february_leap_not_yet()
test_end_of_february_leap_actual()
test_end_of_year()
print(“特定日期逻辑测试通过!”)
示例6:时区
正确测试时区感知的代码,处理偏移和类似BST/GMT的转换。
# 需要 Python 3.9+ 支持 zoneinfo,或者对于旧版本使用 `pip install pytz`
import datetime
from freezegun import freeze_time
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from pytz import timezone as ZoneInfo # 用于旧版本 Python/pytz 的备用方案
def get_local_and_utc_time():
# 假设本地时区为欧洲/伦敦,仅作示例用
local_tz = ZoneInfo(“Europe/London”)
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_local = now_utc.astimezone(local_tz)
print(f” 获取时间:UTC={now_utc}, 本地时间={now_local} ({now_local.tzname()})”)
return now_local, now_utc
# 冻结时间为 UTC 上午 9 点。夏季伦敦是 UTC+1(BST)。10 月 27 日是 BST。
@freeze_time(“2023-10-27 09:00:00”, tz_offset=0) # tz_offset=0 表明冻结的时间字符串是 UTC
def test_time_in_london_bst():
print(”
运行 test_time_in_london_bst:”)
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 10 # 该日期伦敦是 UTC+1
assert local_time.tzname() == “BST”
# 冻结时间为 UTC 上午 9 点。使用 12 月 27 日,这是 GMT(UTC+0)
@freeze_time(“2023-12-27 09:00:00”, tz_offset=0)
def test_time_in_london_gmt():
print(”
运行 test_time_in_london_gmt:”)
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 9 # 该日期伦敦是 UTC+0
assert local_time.tzname() == “GMT”
test_time_in_london_bst()
test_time_in_london_gmt()
print(”
时区测试通过!”)
#
# 输出
#
运行 test_time_in_london_bst:
获取时间:UTC=2023-10-27 09:00:00+00:00, 本地时间=2023-10-27 10:00:00+01:00 (BST)
运行 test_time_in_london_gmt:
获取时间:UTC=2023-12-27 09:00:00+00:00, 本地时间=2023-12-27 09:00:00+00:00 (GMT)
时区测试通过!
# 需要 Python 3.9+ 支持 zoneinfo 或 `pip install pytz` 用于旧版本
import datetime
from freezegun import freeze_time
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from pytz import timezone as ZoneInfo # 用于旧版本 Python/pytz 的备用
def get_local_and_utc_time():
# 假设本地时区为欧洲/伦敦,仅作示例
local_tz = ZoneInfo(“Europe/London”)
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_local = now_utc.astimezone(local_tz)
print(f” 获取时间:UTC={now_utc},本地={now_local}({now_local.tzname()})”)
return now_local, now_utc
# 冻结时间为 UTC 上午 9 点。伦敦夏季为 UTC+1(BST)。10 月 27 日为 BST。
@freeze_time(“2023-10-27 09:00:00”, tz_offset=0) # tz_offset=0 表明冻结的时间字符串为 UTC
def test_time_in_london_bst():
print(”
运行 test_time_in_london_bst:”)
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 10 # 伦敦在这一天为 UTC+1
assert local_time.tzname() == “BST”
# 冻结时间为 UTC 上午 9 点。使用 12 月 27 日,这是 GMT(UTC+0)
@freeze_time(“2023-12-27 09:00:00”, tz_offset=0)
def test_time_in_london_gmt():
print(”
运行 test_time_in_london_gmt:”)
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 9 # 伦敦在这一天为 UTC+0
assert local_time.tzname() == “GMT”
test_time_in_london_bst()
test_time_in_london_gmt()
print(”
时区测试通过!”)
#
# 输出
#
运行 test_time_in_london_bst:
获取时间:UTC=2023-10-27 09:00:00+00:00,本地=2023-10-27 10:00:00+01:00(BST)
运行 test_time_in_london_gmt:
获取时间:UTC=2023-12-27 09:00:00+00:00,本地=2023-12-27 09:00:00+00:00(GMT)
时区测试通过!
示例7:使用move_to函数进行显式时间旅行
在单个测试中跳转到复杂时间序列中的特定时间点。
“`python
import datetime
from freezegun import freeze_time
class ReportGenerator:
def __init__(self):
self.creation_time = datetime.datetime.now()
self.data = {“status”: “pending”, “generated_at”: None}
print(f” 报告创建于 {self.creation_time}”)
def generate(self):
self.data[“status”] = “generated”
self.data[“generated_at”] = datetime.datetime.now()
print(f” 报告生成于 {self.data['generated_at']}”)
def get_status_update(self):
now = datetime.datetime.now()
if self.data[“status”] == “generated”:
time_since_generation = now – self.data[“generated_at”]
status = f”生成于 {
time_since_generation.seconds} 秒前。”
else:
time_since_creation = now – self.creation_time
status = f”等待生成 {
time_since_creation.seconds} 秒。”
print(f” 状态更新于 {now}: '{status}'”)
return status
def test_report_lifecycle():
print(”
运行测试报告生命周期:”)
with freeze_time(“2023-11-01 10:00:00”) as freezer:
report = ReportGenerator()
assert report.data[“status”] == “pending”
# 检查5秒后的状态
target_time = datetime.datetime(2023, 11, 1, 10, 0, 5)
print(f” 移动时间至 {target_time}”)
freezer.move_to(target_time)
assert report.get_status_update() == “等待生成 5 秒。”
# 在10:01:00生成报告
target_time = datetime.datetime(2023, 11, 1, 10, 1, 0)
print(f” 移动时间至 {target_time} 并生成报告”)
freezer.move_to(target_time)
report.generate()
assert report.data[“status”] == “generated”
assert report.get_status_update() == “生成于 0 秒前。”
# 生成后30秒检查状态
target_time = datetime.datetime(2023, 11, 1, 10, 1, 30)
print(f” 移动时间至 {target_time}”)
freezer.move_to(target_time)
assert report.get_status_update() == “生成于 30 秒前。”
print(” 复杂生命周期测试通过!”)
test_report_lifecycle()
# — 失败场景 —
def
test_report_lifecycle_fail_forgot_move():
print(”
— 运行生命周期测试 (失败 – 忘记 move_to) —“)
with freeze_time(“2023-11-01 10:00:00”) as freezer:
report = ReportGenerator()
assert report.data[“status”] == “pending”
# 我们本意是在5秒后检查状态,但忘记移动时间
print(f” 检查状态 (当前时间为 {datetime.datetime.now()})”)
# freezer.move_to(“2023-11-01 10:00:05”) # <– 忘记了!
try:
assert report.get_status_update() == “等待生成 5 秒。”
except AssertionError as e:
print(f” AssertionError: {e}. 如预期般失败.”)
test_report_lifecycle_fail_forgot_move()
“`
“`python
import datetime
from freezegun import freeze_time
class ReportGenerator:
def __init__(self):
self.creation_time = datetime.datetime.now()
self.data = {“status”: “pending”, “generated_at”: None}
print(f” 报告创建于 {self.creation_time}”)
def generate(self):
self.data[“status”] = “generated”
self.data[“generated_at”] = datetime.datetime.now()
print(f” 报告生成于 {self.data['generated_at']}”)
def get_status_update(self):
now = datetime.datetime.now()
if self.data[“status”] == “generated”:
time_since_generation = now – self.data[“generated_at”]
status = f”生成于 {
time_since_generation.seconds} 秒前.”
else:
time_since_creation = now – self.creation_time
status = f”等待中 {
time_since_creation.seconds} 秒.”
print(f” 状态更新于 {now}: '{status}'”)
return status
def test_report_lifecycle():
print(”
运行测试报告生命周期:”)
with freeze_time(“2023-11-01 10:00:00”) as freezer:
report = ReportGenerator()
assert report.data[“status”] == “pending”
# 检查5秒后的状态
target_time = datetime.datetime(2023, 11, 1, 10, 0, 5)
print(f” 移动时间至 {target_time}”)
freezer.move_to(target_time)
assert report.get_status_update() == “等待中 5 秒.”
# 在10:01:00生成报告
target_time = datetime.datetime(2023, 11, 1, 10, 1, 0)
print(f” 移动时间至 {target_time} 并生成报告”)
freezer.move_to(target_time)
report.generate()
assert report.data[“status”] == “generated”
assert report.get_status_update() == “生成于 0 秒前.”
# 生成后30秒检查状态
target_time = datetime.datetime(2023, 11, 1, 10, 1, 30)
print(f” 移动时间至 {target_time}”)
freezer.move_to(target_time)
assert report.get_status_update() == “生成于 30 秒前.”
print(” 复杂生命周期测试通过!”)
test_report_lifecycle()
# — 失败场景 —
def
test_report_lifecycle_fail_forgot_move():
print(”
— 运行生命周期测试 (失败 – 忘记 move_to) —“)
with freeze_time(“2023-11-01 10:00:00”) as freezer:
report = ReportGenerator()
assert report.data[“status”] == “pending”
# 我们本意是在5秒后检查状态,但忘记移动时间
print(f” 检查状态 (当前时间为 {datetime.datetime.now()})”)
# freezer.move_to(“2023-11-01 10:00:05”) # <– 忘记了!
try:
assert report.get_status_update() == “等待中 5 秒.”
except AssertionError as e:
print(f” AssertionError: {e}. 如预期一样失败.”)
test_report_lifecycle_fail_forgot_move()
“`
这是输出。
运行test_report_lifecycle:
报告创建于2023-11-01 10:00:00
将时间移动到2023-11-01 10:00:05
状态更新于2023-11-01 10:00:05:'等待5秒。'
将时间移动到2023-11-01 10:01:00并生成报告
报告生成于2023-11-01 10:01:00
状态更新于2023-11-01 10:01:00:'生成于0秒前。'
将时间移动到2023-11-01 10:01:30
状态更新于2023-11-01 10:01:30:'生成于30秒前。'
复杂的生命周期测试通过!
— 运行生命周期测试(失败-忘记move_to)—
报告创建于2023-11-01 10:00:00
检查状态(时间仍为2023-11-01 10:00:00)
状态更新于2023-11-01 10:00:00:'等待0秒。'
AssertionError:. 预期失败。
运行test_report_lifecycle:
报告创建于2023-11-01 10:00:00
将时间移动到2023-11-01 10:00:05
2023-11-01 10:00:05的状态更新:'等待5秒。'
将时间移动到2023-11-01 10:01:00并生成报告
报告生成于2023-11-01 10:01:00
2023-11-01 10:01:00的状态更新:'生成于0秒前。'
将时间移动到2023-11-01 10:01:30
2023-11-01 10:01:30的状态更新:'生成于30秒前。'
复杂的生命周期测试通过!
— 运行生命周期测试(失败-忘记move_to)—
报告创建于2023-11-01 10:00:00
检查状态(时间仍为2023-11-01 10:00:00)
2023-11-01 10:00:00的状态更新:'等待0秒。'
断言错误:. 如预期那样失败。
摘要
Freezegun 是任何需要测试涉及日期和时间的代码的 Python 开发人员的绝佳工具。它将潜在不稳定、难以编写的测试转变为简单、健壮和确定性的测试。通过让您轻松冻结、前进和穿越时间,并清楚地表明时间不受控制时,它解锁了有效和可靠地测试先前具有挑战性的场景的能力。
为了说明这一点,我提供了几个示例,涵盖了涉及日期和时间测试的不同情况,并展示了使用Freezegun如何消除传统测试框架可能遇到的许多障碍。
虽然我们已经介绍了核心功能,但你可以通过使用Freezegun做更多事情,我提议查看它的GitHub页面。
简而言之,Freezegun 是一个你应该了解并使用的库,如果你的代码涉及时间并且需要进行彻底可靠的测试。
