Better date mocking in Python tests with FreezeGun
A couple of years ago I stumbled across the FreezeGun library. It’s been a great help in making the date/time-related intent in my Python tests much clearer. Since then it’s been in regular use replacing my legacy mocked dates with bright shiny frozen ones.

Treading a well-worn path
The standard pattern I’d been using in my test code was taken directly from
the Python mock
library
documentation.
It turns out I’ve been using this pattern for a long time: while writing
this post I realised that I’ve been using it since before the mock
library
was integrated into the standard library. In other words, since back in the
Python 2 days! Crikey!
Such a tried-and-true (and well-documented) pattern is great. It’s robust and one can lean on it again and again to do the job. Other devs have likely seen the pattern before, so there’s little need to explain the usage to someone new.
The main downside is needing a mock (and knowing how to use mocking well) and that brings with it its own slew of problems.
But before I get too far ahead of myself, you might not be familiar with the partial mocking pattern, so let’s see what it looks like on working code. Also, note that this post only discusses dates; even so, the concepts apply to times as well.
What did sign myself up for?
Imagine you’ve got a service to automatically deliver high-resolution
satellite images to customers. For the discussion
here, let’s also give this service the unimaginative name of img-send
. I’m
sure you can come up with a better one!
The service is subscription-based and you want to make sure that image delivery works for users with an active subscription. This means we don’t want to send images to users whose subscription hasn’t started yet. Nor do we want to send images to users whose subscription has expired. A subscription will therefore need a start and end date, and we only send images to customers if today’s date lies between these dates.
With those basic requirements defined, we can model such a subscription with
a Subscription
class like this:
# img_src/subscription.py
from datetime import date
class Subscription:
def __init__(self, *, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
@property
def is_active(self):
return self.start_date <= date.today() <= self.end_date
where we pass the start and end dates as keyword arguments to the constructor when instantiating an object. We can use this code like so:
from datetime import date
subscription = Subscription(
start_date=date(2023, 12, 1),
end_date=date(2024, 2, 1)
)
Note that we could also check that start_date
doesn’t fall on or later
than end_date
(it doesn’t make sense for the start date to be after the
end date), but I don’t want the example to be too involved.
Ok, given that both dates mentioned in the example above are well in the
past, it’s clear that the is_active
property will return False
:
# check if the subscription is active
subscription.is_active # => False
If a subscription were set up to align with today’s date, then we’d expect
is_active
to return True
(try it out yourself!).
We can codify these expectations in tests:
# tests/test_subscription.py
from unittest import TestCase
from datetime import date
from img_send.subscription import Subscription
class TestSubscription(TestCase):
def test_subscription_is_active_in_subcription_period(self):
start_date = date(2024, 4, 23)
end_date = date(2024, 5, 23)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertTrue(subscription.is_active)
def test_subscription_is_not_active_outside_subcription_period(self):
start_date = date(2023, 12, 1)
end_date = date(2024, 2, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertFalse(subscription.is_active)
where we check that a subscription is active within the given subscription period and is not active outside it.
Stopping the inexorable march of time
There’s one big issue here: while these tests pass on the 23rd of April 2024 (and will continue to do so until the 23rd of May 2024), they will always fail thereafter. This is because time marches relentlessly onwards and thus the simple progression of time will make our test suite inevitably fail.
How can we fix that? The solution is to stop time in its tracks. We must ensure that “today” is always a well-known value that either lies within or outside the given subscription period. As mentioned at the beginning, my go-to solution to do this was partial mocking.
When using the partial mocking pattern, we mock out only that part of the
module we need to change and leave the remaining functionality intact. For
the code that we’re working with here, that means date.today()
should
always return a constant value, whereas date()
should still work as usual.
This is exactly the situation discussed in the Python mock
module’s
partial mocking
documentation.
Adapting the documented example to use the @patch
decorator (we’ll use
this in the test code soon), we end up with code of this form:
@patch('mymodule.date')
def test_mymodule_has_constant_today(self, mock_date):
mock_date.today.return_value = date(2010, 10, 8)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
# ... test code which uses the fact that "today" is 2010-10-08
The line
@patch('mymodule.date')
mocks out the entire date
module as used within
mymodule
1 and adds the mock_date
argument to the test
method.
The next line after the test method definition sets the value to return when
calling date.today()
:
mock_date.today.return_value = date(2010, 10, 8)
i.e. any call to date.today()
within the test method will always return
date(2010, 10, 8)
.
The subsequent line
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
ensures that calling date()
has the usual behaviour and returns a date
object from the given arguments.
Focussing on the differences after having updated the test code, we have:
from unittest import TestCase
+from unittest.mock import patch
from datetime import date
from img_send.subscription import Subscription
class TestSubscription(TestCase):
- def test_subscription_is_active_in_subcription_period(self):
+ @patch('img_send.subscription.date')
+ def test_subscription_is_active_in_subcription_period(self, mock_date):
+ mock_date.today.return_value = date(2024, 5, 3)
+ mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
+
start_date = date(2024, 4, 23)
end_date = date(2024, 5, 23)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertTrue(subscription.is_active)
- def test_subscription_is_not_active_outside_subcription_period(self):
+ @patch('img_send.subscription.date')
+ def test_subscription_is_not_active_outside_subcription_period(
+ self, mock_date
+ ):
+ mock_date.today.return_value = date(2024, 5, 3)
+ mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
+
start_date = date(2023, 12, 1)
end_date = date(2024, 2, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertFalse(subscription.is_active)
To make it very obvious that this works, let’s make all dates lie in the past. The diff after making the change is as follows:
class TestSubscription(TestCase):
@patch('img_send.subscription.date')
def test_subscription_is_active_in_subcription_period(self, mock_date):
- mock_date.today.return_value = date(2024, 5, 3)
+ mock_date.today.return_value = date(2023, 8, 2)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
- start_date = date(2024, 4, 23)
- end_date = date(2024, 5, 23)
+ start_date = date(2023, 7, 1)
+ end_date = date(2023, 9, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
@@ -25,11 +25,11 @@ class TestSubscription(TestCase):
def test_subscription_is_not_active_outside_subcription_period(
self, mock_date
):
- mock_date.today.return_value = date(2024, 5, 3)
+ mock_date.today.return_value = date(2006, 8, 24)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
- start_date = date(2023, 12, 1)
- end_date = date(2024, 2, 1)
+ start_date = date(2006, 7, 1)
+ end_date = date(2006, 8, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
The tests still pass! Yay!
The full test code now looks like this:
# tests/test_subscription.py
from unittest import TestCase
from unittest.mock import patch
from datetime import date
from img_send.subscription import Subscription
class TestSubscription(TestCase):
@patch('img_send.subscription.date')
def test_subscription_is_active_in_subcription_period(self, mock_date):
mock_date.today.return_value = date(2023, 8, 2)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
start_date = date(2023, 7, 1)
end_date = date(2023, 9, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertTrue(subscription.is_active)
@patch('img_send.subscription.date')
def test_subscription_is_not_active_outside_subcription_period(
self, mock_date
):
mock_date.today.return_value = date(2006, 8, 24)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
start_date = date(2006, 7, 1)
end_date = date(2006, 8, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertFalse(subscription.is_active)
Note how this digs around in the module internals: we need to know how the
date
module works and we need to know exactly where date.today()
is used
so that we can override things correctly. This leaks implementation detail
knowledge into our test code, which is where it doesn’t belong. Surely,
there’s a better way, right?
Freezing time
Partial mocking definitely does the job, but it’s rather low-level. We need
to know exactly which function to mock so that it always returns a known
value and we have to make sure that the remaining functionality (in our
case the date()
function) still works as normal. This is a lot of
unnecessary work.
Fortunately, there’s a simple solution: we can freeze time with the
FreezeGun library. In particular, we
can use the freeze_time
decorator as a drop-in replacement for the partial
mocking pattern.
To be able to use FreezeGun, install it via pip
and add it to your
project’s requirements (e.g. in bash
):
$ pip install freezegun
$ pip freeze | grep i freezegun >> requirements.txt
Now, instead of
@patch('img_send.subscription.date')
def test_subscription_is_not_active_outside_subcription_period(
self, mock_date
):
mock_date.today.return_value = date(2006, 8, 24)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
it’s possible to write
@freeze_time('2006-08-24')
That’s wonderful. It’s just like magic.

Our test code now reduces to this:
# tests/test_subscription.py
from unittest import TestCase
from datetime import date
from freezegun import freeze_time
from img_send.subscription import Subscription
class TestSubscription(TestCase):
@freeze_time('2023-08-02')
def test_subscription_is_active_in_subcription_period(self):
start_date = date(2023, 7, 1)
end_date = date(2023, 9, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertTrue(subscription.is_active)
@freeze_time('2006-08-24')
def test_subscription_is_not_active_outside_subcription_period(self):
start_date = date(2006, 7, 1)
end_date = date(2006, 8, 1)
subscription = Subscription(
start_date=start_date, end_date=end_date
)
self.assertFalse(subscription.is_active)
This is such an improvement, I can’t rave about it enough (although, I’m
sure I’ll get over it ).
Frozen and crystal clear
Making this change has made the test code and its associated context much clearer. It’s now obvious up front what date is held constant for the test. Primarily, this clarity is due to the removal of a lot of boilerplate code.
Not only is this cleaner–and more readable–but it’s more general. FreezeGun also handles all the internal date/time-related manipulations and overrides that one would usually need to take care of oneself, namely
all calls to
datetime.datetime.now()
,datetime.datetime.utcnow()
,datetime.date.today()
,time.time()
,time.localtime()
,time.gmtime()
, andtime.strftime()
will return the time that has been frozen.
This is way better than the partial mocking solution. Now one can specify the date without having to meddle with any module internals and one get on with one’s life.
Talking about “just specifying the date”: did you notice how simple it was
to specify the date? FreezeGun handles date-related inputs as strings (among
other things). For instance, it parses date strings into the relevant date
or datetime
object so you don’t have to create the objects explicitly
yourself.
The FreezeGun library is much more flexible than the example I presented above. For instance, to set a constant date for an entire class of tests (i.e. a test suite), it’s not necessary to specify the date for each and every test, one need only decorate the test class, e.g.:
@freeze_time('2023-08-02')
class TestSubscription(TestCase):
# ... rest of test suite code
One can also use freeze_time
with a context manager, making it possible to
set the date/time for a very small portion of a test if desired. It even
handles timezones easily, which is one less headache to worry about.
There are even more features, but the ones I’ve mentioned here are those that I’ve found most useful in my own code. To find out more, have a look at the docs.
Long story short
In short, if you need constant date or time-related information in your tests, don’t reach for a mock, draw your freeze gun.
-
For more information about where to patch, see the
mock
module’s “Where to patch” documentation. ↩
Support
If you liked this post and want to see more like this, please buy me a coffee!
