Mocking Time and Dates in Python
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.