Freezegun patches time functions for deterministic datetime testing. pip install freezegun. from freezegun import freeze_time. Decorator: @freeze_time("2024-01-15"). Context: with freeze_time("2024-06-01 10:30:00"):. Datetime: @freeze_time(datetime(2024, 3, 15, 9, 0, 0)). Time object: @freeze_time("2024-01-01", tick=True) — real time from frozen start. Move: freezer.move_to("2024-06-01") inside context. Patches: datetime.datetime.now(), datetime.datetime.utcnow(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), time.strftime(), time.monotonic(), uuid.uuid1(). Pytest fixture: @pytest.fixture def frozen_time(): with freeze_time("2024-01-01"): yield. Class decorator: @freeze_time("2024-01-01") class TestMyClass: — applies to all methods. Ignore module: @freeze_time("2024-01-01", ignore=["module.name"]). Timezone: @freeze_time("2024-01-01 00:00:00", tz_offset=5). Auto tick: with freeze_time("2024-01-01", auto_tick_seconds=10): — time advances 10s/real-second. Real time: with freeze_time("2024-01-01") as frozen: frozen.move_to("2024-02-01"). Type: isinstance(datetime.datetime.now(), FakeDatetime). Multiple decorators: @freeze_time("2024-01-01") @freeze_time("2024-02-01") — outer wins. Async: works with async def test functions. freeze_time also patches dateutil.parser.parse results. Claude Code generates Freezegun time-controlled test fixtures, expiry logic tests, and cron scheduling tests.
CLAUDE.md for Freezegun
## Freezegun Stack
- Version: freezegun >= 1.4
- Decorator: @freeze_time("YYYY-MM-DD HH:MM:SS") on function or class
- Context: with freeze_time("...") as frozen_time:
- Advances: tick=True (real tempo from frozen start) | auto_tick_seconds=N
- Move: frozen_time.move_to("YYYY-MM-DD") inside context
- Patches: datetime.now/utcnow, date.today, time.time, time.localtime, uuid.uuid1
- Async: works on async def tests automatically
- Ignore: ignore=["third_party.module"] to exclude from freezing
Freezegun Time Control Pipeline
# tests/freezegun_pipeline.py — time control for tests with Freezegun
from __future__ import annotations
import datetime
import time
import uuid
from contextlib import contextmanager
from typing import Generator
import pytest
from freezegun import freeze_time
from freezegun.api import FakeDatetime
# ── 0. Sample code under test ─────────────────────────────────────────────────
# These are the functions we want to test with controlled time.
def get_current_timestamp() -> str:
"""Return ISO 8601 UTC timestamp."""
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
def is_session_expired(created_at: datetime.datetime, ttl_hours: int = 24) -> bool:
"""Check whether a session has expired."""
return datetime.datetime.utcnow() > created_at + datetime.timedelta(hours=ttl_hours)
def days_until_expiry(expires_at: datetime.date) -> int:
"""Return days remaining until an expiry date. Negative = already expired."""
return (expires_at - datetime.date.today()).days
def generate_dated_filename(prefix: str, extension: str = "csv") -> str:
"""Generate filename with today's date: prefix_YYYYMMDD.csv"""
today = datetime.date.today()
return f"{prefix}_{today.strftime('%Y%m%d')}.{extension}"
def greeting_for_time(dt: datetime.datetime = None) -> str:
"""Return Good morning/afternoon/evening based on hour."""
dt = dt or datetime.datetime.now()
hour = dt.hour
if 5 <= hour < 12:
return "Good morning"
elif 12 <= hour < 17:
return "Good afternoon"
elif 17 <= hour < 22:
return "Good evening"
return "Good night"
class TokenBucket:
"""Simple token bucket rate limiter that uses real time."""
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity
self.refill_rate = refill_rate # tokens per second
self.tokens = float(capacity)
self.last_refill = time.monotonic()
def consume(self, n: int = 1) -> bool:
"""Try to consume n tokens. Returns True if allowed."""
now = time.monotonic()
delta = now - self.last_refill
self.tokens = min(self.capacity,
self.tokens + delta * self.refill_rate)
self.last_refill = now
if self.tokens >= n:
self.tokens -= n
return True
return False
class SubscriptionManager:
"""Manages subscriptions with expiry tracking."""
def __init__(self):
self.subscriptions: dict[str, datetime.date] = {}
def subscribe(self, user_id: str, duration_days: int = 30) -> datetime.date:
expires = datetime.date.today() + datetime.timedelta(days=duration_days)
self.subscriptions[user_id] = expires
return expires
def is_active(self, user_id: str) -> bool:
expires = self.subscriptions.get(user_id)
if expires is None:
return False
return datetime.date.today() <= expires
def days_remaining(self, user_id: str) -> int | None:
expires = self.subscriptions.get(user_id)
if expires is None:
return None
return (expires - datetime.date.today()).days
# ── 1. Decorator tests ────────────────────────────────────────────────────────
class TestTimestamps:
@freeze_time("2024-01-15 10:30:00")
def test_get_current_timestamp(self):
result = get_current_timestamp()
assert result == "2024-01-15T10:30:00Z"
@freeze_time("2024-06-01")
def test_date_today(self):
assert datetime.date.today() == datetime.date(2024, 6, 1)
@freeze_time("2024-12-25 00:00:00")
def test_utcnow_frozen(self):
now = datetime.datetime.utcnow()
assert now.year == 2024
assert now.month == 12
assert now.day == 25
@freeze_time("2024-01-15 09:00:00")
def test_greeting_morning(self):
assert greeting_for_time() == "Good morning"
@freeze_time("2024-01-15 14:00:00")
def test_greeting_afternoon(self):
assert greeting_for_time() == "Good afternoon"
@freeze_time("2024-01-15 20:00:00")
def test_greeting_evening(self):
assert greeting_for_time() == "Good evening"
class TestSessionExpiry:
@freeze_time("2024-01-15 10:00:00")
def test_fresh_session_not_expired(self):
created = datetime.datetime(2024, 1, 15, 9, 0, 0) # 1 hour ago
assert not is_session_expired(created, ttl_hours=24)
@freeze_time("2024-01-16 11:00:00")
def test_expired_session(self):
created = datetime.datetime(2024, 1, 15, 9, 0, 0) # 26 hours ago
assert is_session_expired(created, ttl_hours=24)
@freeze_time("2024-01-15 09:00:01")
def test_barely_not_expired(self):
"""Session created 24 hours minus 1 second ago — still valid."""
created = datetime.datetime(2024, 1, 14, 9, 0, 2)
assert not is_session_expired(created, ttl_hours=24)
@freeze_time("2024-01-15 09:00:00")
def test_exactly_on_boundary(self):
"""Session created exactly 24 hours ago — expired."""
created = datetime.datetime(2024, 1, 14, 9, 0, 0)
assert is_session_expired(created, ttl_hours=24)
# ── 2. Context manager tests ──────────────────────────────────────────────────
class TestContextManager:
def test_filename_generation(self):
with freeze_time("2024-06-15"):
filename = generate_dated_filename("report")
assert filename == "report_20240615.csv"
def test_days_until_expiry_future(self):
with freeze_time("2024-01-01"):
future = datetime.date(2024, 1, 11)
assert days_until_expiry(future) == 10
def test_days_until_expiry_past(self):
with freeze_time("2024-06-01"):
past = datetime.date(2024, 5, 29)
assert days_until_expiry(past) == -3
def test_time_time_frozen(self):
"""time.time() is also frozen."""
with freeze_time("2024-01-01 00:00:00"):
t = time.time()
assert t == pytest.approx(1704067200.0) # Unix timestamp for 2024-01-01 UTC
def test_uuid1_frozen(self):
"""uuid.uuid1() uses frozen time — two calls in same freeze give related UUIDs."""
with freeze_time("2024-01-01"):
u1 = uuid.uuid1()
u2 = uuid.uuid1()
# Both UUIDs have the same time component
assert u1.time == u2.time
def test_move_to(self):
"""move_to() shifts the frozen time within a context."""
with freeze_time("2024-01-01") as frozen:
assert datetime.date.today() == datetime.date(2024, 1, 1)
frozen.move_to("2024-06-15")
assert datetime.date.today() == datetime.date(2024, 6, 15)
# ── 3. Subscription lifecycle tests ──────────────────────────────────────────
class TestSubscriptionManager:
"""Test temporal business logic with time travel."""
def test_new_subscription_is_active(self):
mgr = SubscriptionManager()
with freeze_time("2024-01-01"):
mgr.subscribe("user_1", duration_days=30)
with freeze_time("2024-01-15"):
assert mgr.is_active("user_1") is True
def test_subscription_expires(self):
mgr = SubscriptionManager()
with freeze_time("2024-01-01"):
mgr.subscribe("user_1", duration_days=30) # expires 2024-01-31
with freeze_time("2024-02-01"):
assert mgr.is_active("user_1") is False
def test_days_remaining_midway(self):
mgr = SubscriptionManager()
with freeze_time("2024-01-01"):
mgr.subscribe("user_1", duration_days=30)
with freeze_time("2024-01-11"):
remaining = mgr.days_remaining("user_1")
assert remaining == 20
def test_renewal_extends_from_today(self):
mgr = SubscriptionManager()
with freeze_time("2024-01-01"):
mgr.subscribe("user_1", duration_days=30)
# Renew close to expiry
with freeze_time("2024-01-28"):
new_expiry = mgr.subscribe("user_1", duration_days=30)
assert new_expiry == datetime.date(2024, 2, 27)
# ── 4. Class-level freeze ─────────────────────────────────────────────────────
@freeze_time("2024-03-15 12:00:00")
class TestAllFrozenToMarch:
"""All test methods in this class share the same frozen time."""
def test_a(self):
assert datetime.date.today() == datetime.date(2024, 3, 15)
def test_b(self):
assert datetime.datetime.utcnow().hour == 12
def test_c(self):
assert get_current_timestamp() == "2024-03-15T12:00:00Z"
# ── 5. pytest fixture approach ────────────────────────────────────────────────
@pytest.fixture
def fixed_today():
"""Reusable fixture: pin date to 2024-01-01 for any test."""
with freeze_time("2024-01-01 09:00:00") as frozen:
yield frozen
class TestWithFixture:
def test_uses_fixed_today(self, fixed_today):
assert datetime.date.today() == datetime.date(2024, 1, 1)
def test_move_within_fixture(self, fixed_today):
fixed_today.move_to("2024-12-31")
assert datetime.date.today() == datetime.date(2024, 12, 31)
# ── Demo ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("Freezegun Time Control Demo")
print("=" * 50)
print("\n1. Decorator freeze:")
@freeze_time("2024-06-15 08:00:00")
def show_frozen():
print(f" now() = {datetime.datetime.now()}")
print(f" today() = {datetime.date.today()}")
print(f" time() = {time.time()}")
show_frozen()
print("\n2. Context manager with move_to:")
with freeze_time("2024-01-01") as frozen:
print(f" start: {datetime.date.today()}")
frozen.move_to("2024-07-04")
print(f" after move: {datetime.date.today()}")
print("\n3. Subscription expiry:")
mgr = SubscriptionManager()
with freeze_time("2024-01-01"):
mgr.subscribe("alice", 30)
with freeze_time("2024-01-20"):
print(f" alice active Jan 20: {mgr.is_active('alice')} ({mgr.days_remaining('alice')} days left)")
with freeze_time("2024-02-05"):
print(f" alice active Feb 5: {mgr.is_active('alice')}")
For the unittest.mock.patch("datetime.datetime") alternative — patching datetime.datetime directly requires replacing datetime.datetime.now with a Mock, handling datetime.date separately, patching time.time in a third call, and re-patching if any transitive import also uses datetime, while @freeze_time("2024-01-15") patches all time surfaces (datetime.now/utcnow, date.today, time.time, time.localtime, time.monotonic, uuid.uuid1) in a single decorator with no mock setup code. For the pytest-freezegun alternative — pytest-freezegun is a thin wrapper that requires the freezer fixture name while using Freezegun directly with @freeze_time on the class applies to every method at once, frozen_time.move_to() shifts time mid-test for expiry boundary tests, and tick=True lets you write real-sleep assertions while starting from a deterministic timestamp. The Claude Skills 360 bundle includes Freezegun skill sets covering @freeze_time decorator, context manager with move_to, class-level freeze, pytest fixture pattern, session and subscription expiry tests, time.time/uuid.uuid1 coverage, greeting hour tests, dated filename generation, boundary condition tests, and auto_tick_seconds for sleep-based code. Start with the free tier to try time-control testing code generation.