Skip to main content

Mocking Time and Dates in Python

· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Mocking time and dates is a common and often essential practice when writing tests for time-dependent logic. If your code's behavior changes based on the current time (e.g., a function that checks for a subscription's expiration, a daily report generator, or a cache invalidation rule), relying on the system's clock will lead to unreliable and non-repeatable tests.

You need a way to "freeze" time or fast-forward it to a specific point so your tests run in a controlled, predictable environment. Python's unittest.mock.patch is the perfect tool for this, allowing you to replace datetime.now() with a mock that returns a fixed value.

The Problem with Time-Dependent Code ⏰

Consider a simple function that checks if a user's subscription is still active.

subscription_service.py

import datetime

def is_subscription_active(end_date):
"""
Checks if a subscription is active based on the current date.

Args:
end_date (datetime.date): The end date of the subscription.

Returns:
bool: True if the subscription is active, False otherwise.
"""
return datetime.date.today() <= end_date

If you test this function today, it might pass, but if you run the same test tomorrow, it might fail. This is because datetime.date.today() is a live, unpredictable value. To write a reliable test, you need to control the value of datetime.date.today().


Mocking datetime.date.today()

The solution is to use patch to replace datetime.date.today() with a MagicMock object that returns a fixed date. The key is to know where to patch: you need to patch datetime.date as it is referenced by the module you are testing.

test_subscription_service.py

import unittest
from unittest.mock import patch
import datetime
from subscription_service import is_subscription_active

class TestSubscriptionService(unittest.TestCase):

# We patch `datetime.date` as it is imported in `subscription_service`
@patch('subscription_service.datetime.date')
def test_subscription_is_active(self, mock_date):
# Configure the mock to return a specific date when .today() is called
mock_date.today.return_value = datetime.date(2025, 1, 15)

# Test a subscription that ends after our mocked date
end_date = datetime.date(2025, 1, 20)
self.assertTrue(is_subscription_active(end_date))

@patch('subscription_service.datetime.date')
def test_subscription_has_expired(self, mock_date):
# Set the mocked date to be after the subscription's end date
mock_date.today.return_value = datetime.date(2025, 1, 30)

# Test a subscription that has expired
end_date = datetime.date(2025, 1, 20)
self.assertFalse(is_subscription_active(end_date))

In this example, @patch('subscription_service.datetime.date') replaces the date class within the subscription_service module. We then set the return_value of the mock's today method, allowing us to simulate different time scenarios.


Mocking datetime.datetime.now()

Another common scenario involves mocking the current time, including the hour, minute, and second. The process is similar, but you patch the datetime module's datetime class.

Example: Checking Time-Based Caching

Imagine a function that decides whether to return a cached item based on a timestamp.

caching_service.py

import datetime

def is_cache_valid(last_updated_time, cache_duration_minutes=60):
"""Checks if a cache is still valid."""
now = datetime.datetime.now()
if (now - last_updated_time).total_seconds() > cache_duration_minutes * 60:
return False
return True

Here's how to test this by mocking datetime.datetime.now().

test_caching_service.py

import unittest
from unittest.mock import patch, MagicMock
import datetime
from caching_service import is_cache_valid

class TestCachingService(unittest.TestCase):

@patch('caching_service.datetime.datetime')
def test_cache_is_valid(self, mock_datetime):
# Create a fixed mock time
mock_datetime.now.return_value = datetime.datetime(2025, 5, 1, 10, 0, 0)

# A recent timestamp, so the cache is valid
last_updated = datetime.datetime(2025, 5, 1, 9, 30, 0)

self.assertTrue(is_cache_valid(last_updated))

@patch('caching_service.datetime.datetime')
def test_cache_is_expired(self, mock_datetime):
# Move our mock time forward
mock_datetime.now.return_value = datetime.datetime(2025, 5, 1, 11, 30, 1)

# The cache should be expired
last_updated = datetime.datetime(2025, 5, 1, 10, 30, 0)

self.assertFalse(is_cache_valid(last_updated, cache_duration_minutes=60))

By mocking datetime.datetime.now(), you can simulate any time-based scenario, from a cache that has just been created to one that expired exactly one second ago. This guarantees that your tests will be deterministic and reliable, regardless of when you run them.


The freezegun Library

For more complex time-related tests, manually patching datetime can become cumbersome. The third-party library freezegun offers a more convenient and readable way to mock time. It works as a decorator or a context manager and automatically handles all the patching for you.

test_with_freezegun.py

import unittest
import datetime
from freezegun import freeze_time
from subscription_service import is_subscription_active

class TestWithFreezegun(unittest.TestCase):

@freeze_time("2025-01-15")
def test_subscription_is_active_with_freezegun(self):
end_date = datetime.date(2025, 1, 20)
self.assertTrue(is_subscription_active(end_date))

@freeze_time("2025-01-30")
def test_subscription_has_expired_with_freezegun(self):
end_date = datetime.date(2025, 1, 20)
self.assertFalse(is_subscription_active(end_date))

Using freezegun simplifies the code and makes the intent of the test immediately clear. While unittest.mock is sufficient for most cases, freezegun is a great tool to have in your toolbox for more extensive time-based testing.

Sources