Python Testing Tip #12 / Nov. 27, 2025

Time Travel in Your Tests with freezegun

Testing time-dependent code is notoriously tricky. If your code checks whether a subscription is active, validates trial eligibility, or performs any date calculations, your tests can become flaky and unreliable. Enter freezegun – a library that lets you freeze time in your tests.

The Problem

Consider testing a subscription that expires after 30 days. How do you reliably verify it’s active on day 15 but expired on day 45? You could create subscriptions that expire far in the future, but that’s fragile. What you really want is to control what datetime.now() returns.

The Solution

freezegun patches datetime.now(), date.today(), and other time functions so they return a fixed value during your test. Here’s everything you need to know:

# test_subscription.py
from dataclasses import dataclass
from datetime import datetime, timedelta

from freezegun import freeze_time

@dataclass
class Subscription:
    plan: str
    started_at: datetime
    duration_days: int

    @property
    def expires_at(self) -> datetime:
        return self.started_at + timedelta(days=self.duration_days)

    def is_active(self) -> bool:
        return datetime.now() < self.expires_at

    def days_remaining(self) -> int:
        if not self.is_active():
            return 0
        return (self.expires_at - datetime.now()).days

# 1. Basic usage: freeze time with a decorator
@freeze_time("2024-01-15")
def test_subscription_is_active():
    subscription = Subscription(plan="premium", started_at=datetime(2024, 1, 1), duration_days=30)
    assert subscription.is_active() is True
    assert subscription.days_remaining() == 16

# 2. Context manager for testing multiple time points
def test_trial_eligibility():
    registered_at = datetime(2024, 3, 1)

    with freeze_time("2024-03-05"):
        days_since = (datetime.now() - registered_at).days
        assert days_since <= 7  # eligible

    with freeze_time("2024-03-15"):
        days_since = (datetime.now() - registered_at).days
        assert days_since > 7  # not eligible

# 3. Moving through time within a single test
def test_subscription_lifecycle():
    with freeze_time("2024-01-01") as frozen_time:
        subscription = Subscription(plan="basic", started_at=datetime(2024, 1, 1), duration_days=7)

        assert subscription.is_active() is True
        assert subscription.days_remaining() == 7

        frozen_time.move_to("2024-01-05")
        assert subscription.days_remaining() == 3

        frozen_time.move_to("2024-01-10")
        assert subscription.is_active() is False

# 4. Freeze with specific time (useful for time-of-day logic)
@freeze_time("2024-06-15 14:30:00")
def test_specific_time():
    assert datetime.now().hour == 14
    assert datetime.now().minute == 30

# 5. Allow time to tick forward (useful for timeout tests)
@freeze_time("2024-01-01 12:00:00", tick=True)
def test_time_ticks_forward():
    time_1 = datetime.now()
    time_2 = datetime.now()
    assert time_2 >= time_1

Key Takeaways

  • Use the @freeze_time decorator for simple, single-point-in-time tests
  • Use a context manager when testing the same logic at multiple time points
  • Use move_to() to simulate time passing within a test
  • Enable tick=True when you need time to actually progress
  • Your tests become deterministic and reproducible – no more flaky time-based failures

Install with: pip install freezegun

Share this tip

The complete testing system, not just tips.

Stop piecing together advice from blog posts. This course gives you a structured approach to Python testing that scales with your codebase and keeps your AI agents in check.

Get the Course $20