Esa prueba de nuestro código es una parte crucial y esencial del ciclo de vida del desarrollo de software. Esto es quizás aún más importante cuando discutimos los sistemas de IA y ML, donde una incertidumbre inherente y un elemento alucinatorio ya están potencialmente horneados desde el principio.
Y dentro de ese marco de prueba general, el código de prueba que se comporta de manera diferente en función de la fecha o hora actual puede ser un dolor de cabeza real. ¿Cómo verifica de manera confiable la lógica que se desencadena solo a la medianoche, calcula las fechas relativas (“hace 2 horas”), o maneja situaciones difíciles como los años de salto o finales de mes? Se burla manualmente el módulo de fecha de fecha de Python puede ser engorroso y propenso a errores.
Si alguna vez has luchado con esto, no estás solo. Pero, ¿qué pasaría si pudieras … detener el tiempo? ¿O incluso viajar por él dentro de sus pruebas?
Eso es precisamente lo que la biblioteca Freezegun le permite hacer. Es una solución elegante para un problema de prueba común, sin embargo, muchos desarrolladores de Python experimentados nunca han oído hablar de él.
Freezegun Permite que sus pruebas de Python simulen momentos específicos en el tiempo burlándose de los módulos Python de fecha y hora, hora, y péndulo. Es simple de usar pero potente para crear pruebas deterministas y confiables para un código sensible al tiempo.
¿Por qué Freezegun es tan útil?
- Determinismo. Este es el beneficio principal de Freezegun. Las pruebas que involucran el tiempo se vuelven completamente predecibles. Ejecutar DateTime.now () dentro de un bloque congelado devuelve la misma marca de tiempo congelada, eliminando las pruebas escamosas causadas por diferencias de milisegundos o vueltas de fecha durante la ejecución de la prueba.
- Sencillez. En comparación con el parche manual DateTime. ahora o usar unittest.mockFreezegun a menudo es mucho más limpio y requiere menos código calderoso, especialmente cuando cambia temporalmente el tiempo.
- Viaje en el tiempo. Simule fácilmente fechas y tiempos específicos: pasado, presente o futuro. Esto es crucial para probar casos de borde, como el procesamiento de fin de año, segundos de salto, transiciones de tiempo de verano, o simplemente verificar la lógica vinculada a eventos específicos.
- Prueba de tiempo relativo. Las funciones de prueba que calculan los tiempos relativos (por ejemplo, “expira en 3 días”) mediante el tiempo de congelación y creando marcas de tiempo en relación con ese momento congelado.
- Tick Tock. Freezegun permite que avance el tiempo (“marque”) desde el momento congelado dentro de una prueba, que es perfecta para probar tiempos de espera, duraciones o secuencias de eventos dependientes del tiempo.
Con suerte, te he convencido de que Freezegun podría ser una valiosa adición a tu caja de herramientas de Python. Veamos en acción mirando algunos fragmentos de código de muestra.
Configurar un entorno de desarrollo
Pero antes de eso, establezcamos un entorno de desarrollo para experimentar. Utilizo Miniconda para esto, pero puedes usar cualquier herramienta con la que estés familiarizado.
Soy un usuario de Windows, pero a menudo me desarrollaré usando WSL2 Ubuntu para Windows, que es lo que haré aquí.
Todo el código que muestre debería funcionar igualmente bien en Windows o sistemas operativos similares a UNIX.
# Create and activate a new dev environment
#
(base) $ conda create -n freezegun python=3.12 -y
(base) $ conda activate freezegun
Ahora, podemos instalar las bibliotecas necesarias restantes.
(freezegun) $ pip install freezegun jupyter
Usaré el cuaderno Jupyter para ejecutar mi código. Para seguir, escriba jupyter notebook en su símbolo del sistema. Debería ver un cuaderno Jupyter abierto en su navegador. Si eso no sucede automáticamente, es probable que vea una pantalla de información después del jupyter notebook dominio. Cerca de la parte inferior, encontrará una URL para copiar y pegar en su navegador para iniciar el cuaderno Jupyter.
Su URL será diferente a la mía, pero debería verse algo así:-
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
Un poco aparte: el código que estoy mostrando en mis ejemplos a continuación hace un uso intensivo de la pitón afirmar dominio. Si no ha encontrado esta función antes o no ha realizado muchas pruebas unitarias en Python, Afirmar se usa para probar si una condición es verdadera, y si no es así, plantea un
AssertionError.Esto ayuda a atrapar problemas durante el desarrollo y se usa comúnmente para depurar y validar supuestos en el código.
Ejemplo 1: Congelamiento de tiempo básico usando un decorador
La forma más común de usar Freezegun es a través de su decorador., @Freeze_Time, que te permite “Establezca” una hora particular del día para probar varias funciones relacionadas con el tiempo.
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 "Good morning!"
elif 12 <= now.hour < 18:
return "Good afternoon!"
else:
return "Good evening!"
# Test the morning greeting
@freeze_time("2023-10-27 09:00:00")
def test_morning_greeting():
print("Running test_morning_greeting:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
assert greeting == "Good morning!"
# Test the evening greeting
@freeze_time("2023-10-27 21:30:00")
def test_evening_greeting():
print("\nRunning test_evening_greeting:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
assert greeting == "Good evening!"
# Run the tests
test_morning_greeting()
test_evening_greeting()
print("\nBasic decorator tests passed!")
# --- Failure Scenario ---
# What happens if we don't freeze time?
print("\n--- Running without freeze_time (might fail depending on actual time) ---")
def test_morning_greeting_unfrozen():
print("Running test_morning_greeting_unfrozen:")
greeting = get_greeting()
print(f" -> Got greeting: '{greeting}'")
# This assertion is now unreliable! It depends on when you run the code.
try:
assert greeting == "Good morning!"
print(" (Passed by chance)")
except AssertionError:
print(" (Failed as expected - time wasn't 9 AM)")
test_morning_greeting_unfrozen()
Y la salida.
Running test_morning_greeting:
Inside get_greeting(), now = 2023-10-27 09:00:00
-> Got greeting: 'Good morning!'
Running test_evening_greeting:
Inside get_greeting(), now = 2023-10-27 21:30:00
-> Got greeting: 'Good evening!'
Basic decorator tests passed!
--- Running without freeze_time (might fail depending on actual time) ---
Running test_morning_greeting_unfrozen:
Inside get_greeting(), now = 2025-04-16 15:00:37.363367
-> Got greeting: 'Good afternoon!'
(Failed as expected - time wasn't 9 AM)
Ejemplo 2: Congelamiento de tiempo básico utilizando un administrador de contexto
Cree un “bloque” de tiempo congelado.
import datetime
from freezegun import freeze_time
def process_batch_job():
start_time = datetime.datetime.now()
# Simulate work
end_time = datetime.datetime.now() # In reality, time would pass
print(f" Inside job: Start={start_time}, End={end_time}") # Added print
return (start_time, end_time)
def test_job_timestamps_within_frozen_block():
print("\nRunning test_job_timestamps_within_frozen_block:")
frozen_time_str = "2023-11-15 10:00:00"
with freeze_time(frozen_time_str):
print(f" Entering frozen block at {frozen_time_str}")
start, end = process_batch_job()
print(f" Asserting start == end: {start} == {end}")
assert start == end
print(f" Asserting start == frozen time: {start} == {datetime.datetime(2023, 11, 15, 10, 0, 0)}")
assert start == datetime.datetime(2023, 11, 15, 10, 0, 0)
print(" Assertions inside block passed.")
print(" Exited frozen block.")
now_outside = datetime.datetime.now()
print(f" Time outside block: {now_outside} (should be real time)")
# This assertion just shows time is unfrozen, value depends on real time
assert now_outside != datetime.datetime(2023, 11, 15, 10, 0, 0)
test_job_timestamps_within_frozen_block()
print("\nContext manager test passed!")
La salida.
Running test_job_timestamps_within_frozen_block:
Entering frozen block at 2023-11-15 10:00:00
Inside job: Start=2023-11-15 10:00:00, End=2023-11-15 10:00:00
Asserting start == end: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Asserting start == frozen time: 2023-11-15 10:00:00 == 2023-11-15 10:00:00
Assertions inside block passed.
Exited frozen block.
Time outside block: 2025-04-16 15:10:15.231632 (should be real time)
Context manager test passed!
Ejemplo 3: Tiempo de avance con Tick
Simule el tiempo que pasa dentro de un período congelado.
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
# --- Manual ticking using context manager ---
def test_event_expiry_manual_tick():
print("\nRunning 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)
# Advance time by 61 seconds
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.")
# --- Failure Scenario ---
@freeze_time("2023-10-27 12:00:00") # No tick=True or manual tick
def test_event_expiry_fail_without_tick():
print("\n--- 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}")
# Simulate work or waiting - without tick, time doesn't advance!
time.sleep(0.1)
print(f" Time after simulated wait: {datetime.datetime.now()}")
print(" Checking expiry (incorrectly, time didn't move):")
try:
# This should ideally be True, but will be False without ticking
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.")
# Run both tests
test_event_expiry_manual_tick()
test_event_expiry_fail_without_tick()
Esto genera lo siguiente.
Running test_event_expiry_manual_tick:
Event created at: 2023-10-27 12:00:00
Checking immediately after creation:
Checking expiry: Now=2023-10-27 12:00:00, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
Ticking forward by 0:01:01...
Time after ticking: 2023-10-27 12:01:01
Checking after ticking:
Checking expiry: Now=2023-10-27 12:01:01, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=True
Manual tick test finished.
--- Running test_event_expiry_fail_without_tick (EXPECT ASSERTION ERROR) ---
Event created at: 2023-10-27 12:00:00
Time after simulated wait: 2023-10-27 12:00:00
Checking expiry (incorrectly, time didn't move):
Checking expiry: Now=2023-10-27 12:00:00, Event=2023-10-27 12:00:00, ExpiresAt=2023-10-27 12:01:00 -> Expired=False
AssertionError: Event did not expire, as expected without tick.
Failure scenario finished.
Ejemplo 4: Pruebas de fechas relativas
Freezegun garantiza la lógica estable de “tiempo hace”.
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} days ago"
elif delta.seconds >= 3600:
hours = delta.seconds // 3600
rel_time_str = f"{hours} hours ago"
elif delta.seconds >= 60:
minutes = delta.seconds // 60
rel_time_str = f"{minutes} minutes ago"
else:
rel_time_str = "just now"
print(f" Formatting relative time: Now={now}, Timestamp={timestamp} -> '{rel_time_str}'")
return rel_time_str
@freeze_time("2023-10-27 15:00:00")
def test_relative_time_formatting():
print("\nRunning test_relative_time_formatting:")
# Event happened 2 days and 3 hours ago relative to frozen time
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
assert format_relative_time(past_event) == "2 days ago"
# Event happened 45 minutes ago
recent_event = datetime.datetime.now() - datetime.timedelta(minutes=45)
assert format_relative_time(recent_event) == "45 minutes ago"
# Event happened just now
current_event = datetime.datetime.now() - datetime.timedelta(seconds=10)
assert format_relative_time(current_event) == "just now"
print(" Relative time tests passed!")
test_relative_time_formatting()
# --- Failure Scenario ---
print("\n--- Running relative time without freeze_time (EXPECT FAILURE) ---")
def test_relative_time_unfrozen():
# Use the same past event timestamp
past_event = datetime.datetime(2023, 10, 25, 12, 0, 0)
print(f" Testing with past_event = {past_event}")
# This will compare against the *actual* current time, not Oct 27th, 2023
formatted_time = format_relative_time(past_event)
try:
assert formatted_time == "2 days ago"
except AssertionError:
# The actual difference will be much larger!
print(f" AssertionError: Expected '2 days ago', but got '{formatted_time}'. Failed as expected.")
test_relative_time_unfrozen()
La salida.
Running test_relative_time_formatting:
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-25 12:00:00 -> '2 days ago'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:15:00 -> '45 minutes ago'
Formatting relative time: Now=2023-10-27 15:00:00, Timestamp=2023-10-27 14:59:50 -> 'just now'
Relative time tests passed!
--- Running relative time without freeze_time (EXPECT FAILURE) ---
Testing with past_event = 2023-10-25 12:00:00
Formatting relative time: Now=2023-10-27 12:00:00, Timestamp=2023-10-25 12:00:00 -> '2 days ago'
Ejemplo 5: Manejo de fechas específicas (fin de mes)
Prueba de casos de borde, como años de salto, de manera confiable.
import datetime
from freezegun import 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" Checking if {check_date} is last day of month: Next day={next_day}, IsLast={is_last}")
return is_last
print("\nRunning specific date logic tests:")
@freeze_time("2023-02-28") # Non-leap year
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") # Leap year
def test_end_of_february_leap_not_yet():
today = datetime.date.today()
assert is_last_day_of_month(today) is False # Feb 29th exists
@freeze_time("2024-02-29") # Leap year - last day
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("Specific date logic tests passed!")
#
# Output
#
Running specific date logic tests:
Checking if 2023-02-28 is last day of month: Next day=2023-03-01, IsLast=True
Checking if 2024-02-28 is last day of month: Next day=2024-02-29, IsLast=False
Checking if 2024-02-29 is last day of month: Next day=2024-03-01, IsLast=True
Checking if 2023-12-31 is last day of month: Next day=2024-01-01, IsLast=True
pecific date logic tests passed!
Ejemplo 6: zonas horarias
Pruebe el código de la zona horaria correctamente, manejando compensaciones y transiciones como BST/GMT.
# Requires Python 3.9+ for zoneinfo or `pip install pytz` for older versions
import datetime
from freezegun import freeze_time
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from pytz import timezone as ZoneInfo # Fallback for older Python/pytz
def get_local_and_utc_time():
# Assume local timezone is Europe/London for this example
local_tz = ZoneInfo("Europe/London")
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_local = now_utc.astimezone(local_tz)
print(f" Getting times: UTC={now_utc}, Local={now_local} ({now_local.tzname()})")
return now_local, now_utc
# Freeze time as 9 AM UTC. London is UTC+1 in summer (BST). Oct 27 is BST.
@freeze_time("2023-10-27 09:00:00", tz_offset=0) # tz_offset=0 means the frozen time string IS UTC
def test_time_in_london_bst():
print("\nRunning test_time_in_london_bst:")
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 10 # London is UTC+1 on this date
assert local_time.tzname() == "BST"
# Freeze time as 9 AM UTC. Use December 27th, which is GMT (UTC+0)
@freeze_time("2023-12-27 09:00:00", tz_offset=0)
def test_time_in_london_gmt():
print("\nRunning test_time_in_london_gmt:")
local_time, utc_time = get_local_and_utc_time()
assert utc_time.hour == 9
assert local_time.hour == 9 # London is UTC+0 on this date
assert local_time.tzname() == "GMT"
test_time_in_london_bst()
test_time_in_london_gmt()
print("\nTimezone tests passed!")
#
# Output
#
Running test_time_in_london_bst:
Getting times: UTC=2023-10-27 09:00:00+00:00, Local=2023-10-27 10:00:00+01:00 (BST)
Running test_time_in_london_gmt:
Getting times: UTC=2023-12-27 09:00:00+00:00, Local=2023-12-27 09:00:00+00:00 (GMT)
Timezone tests passed!
Ejemplo 7: Viaje en el tiempo explícito con la función Move_TO
Saltar entre puntos de tiempo específicos en una sola prueba para secuencias temporales complejas.
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" Report created at {self.creation_time}")
def generate(self):
self.data["status"] = "generated"
self.data["generated_at"] = datetime.datetime.now()
print(f" Report generated at {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"Generated {time_since_generation.seconds} seconds ago."
else:
time_since_creation = now - self.creation_time
status = f"Pending for {time_since_creation.seconds} seconds."
print(f" Status update at {now}: '{status}'")
return status
def test_report_lifecycle():
print("\nRunning test_report_lifecycle:")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.data["status"] == "pending"
# Check status after 5 seconds
target_time = datetime.datetime(2023, 11, 1, 10, 0, 5)
print(f" Moving time to {target_time}")
freezer.move_to(target_time)
assert report.get_status_update() == "Pending for 5 seconds."
# Generate the report at 10:01:00
target_time = datetime.datetime(2023, 11, 1, 10, 1, 0)
print(f" Moving time to {target_time} and generating report")
freezer.move_to(target_time)
report.generate()
assert report.data["status"] == "generated"
assert report.get_status_update() == "Generated 0 seconds ago."
# Check status 30 seconds after generation
target_time = datetime.datetime(2023, 11, 1, 10, 1, 30)
print(f" Moving time to {target_time}")
freezer.move_to(target_time)
assert report.get_status_update() == "Generated 30 seconds ago."
print(" Complex lifecycle test passed!")
test_report_lifecycle()
# --- Failure Scenario ---
def test_report_lifecycle_fail_forgot_move():
print("\n--- Running lifecycle test (FAIL - forgot move_to) ---")
with freeze_time("2023-11-01 10:00:00") as freezer:
report = ReportGenerator()
assert report.data["status"] == "pending"
# We INTEND to check status after 5 seconds, but FORGET to move time
print(f" Checking status (time is still {datetime.datetime.now()})")
# freezer.move_to("2023-11-01 10:00:05") # <-- Forgotten!
try:
assert report.get_status_update() == "Pending for 5 seconds."
except AssertionError as e:
print(f" AssertionError: {e}. Failed as expected.")
test_report_lifecycle_fail_forgot_move()
Aquí está la salida.
Running test_report_lifecycle:
Report created at 2023-11-01 10:00:00
Moving time to 2023-11-01 10:00:05
Status update at 2023-11-01 10:00:05: 'Pending for 5 seconds.'
Moving time to 2023-11-01 10:01:00 and generating report
Report generated at 2023-11-01 10:01:00
Status update at 2023-11-01 10:01:00: 'Generated 0 seconds ago.'
Moving time to 2023-11-01 10:01:30
Status update at 2023-11-01 10:01:30: 'Generated 30 seconds ago.'
Complex lifecycle test passed!
--- Running lifecycle test (FAIL - forgot move_to) ---
Report created at 2023-11-01 10:00:00
Checking status (time is still 2023-11-01 10:00:00)
Status update at 2023-11-01 10:00:00: 'Pending for 0 seconds.'
AssertionError: . Failed as expected.
Resumen
Freezegun es una herramienta fantástica para cualquier desarrollador de Python que necesite probar el código que involucre fechas y horarios. Transforma las pruebas potencialmente escamosas y difíciles de escribir en las simples, robustas y deterministas. Permitiéndole congelar, marcar y viajar a través del tiempo con facilidad, y dejarlo en claro cuando el tiempo no es Controlado: desbloquea la capacidad de probar escenarios previamente desafiantes de manera efectiva y confiable.
Para ilustrar esto, proporcioné varios ejemplos que cubren diferentes instancias que involucran pruebas de fecha y hora y mostré cómo el uso de Freezegun elimina muchos de los obstáculos que podría encontrar un marco de prueba tradicional.
Si bien hemos cubierto las funcionalidades básicas, puede hacer más con Freezegun, y le recomiendo revisar su Página de Github.
En resumen, FreezeGun es una biblioteca que debe conocer y usar si su código trata con el tiempo y necesita probarla de manera exhaustiva y confiable.