Better date mocking in Python tests with FreezeGun
Tags: python, testing
Published: 2024-04-25 16:43:21

---
title: Better date mocking in Python tests with FreezeGun
published: true
date: 2024-04-24 22:00:00 UTC
tags: Python,Testing
canonical_url: https://peateasea.de/better-date-mocking-in-python-tests-with-freezegun/
cover_image: https://peateasea.de/assets/images/atomic-freeze-ray-gun-by-edgar-montes.webp
---

*Originally published on [peateasea.de](https://peateasea.de/better-date-mocking-in-python-tests-with-freezegun/).*

A couple of years ago I stumbled across the [FreezeGun](https://pypi.org/project/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](https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking). 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](https://icysea.app/). 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:

```python
# 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:

```python
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`:

```python
# 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:

```python
# 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](https://docs.python.org/3/library/unittest.mock-examples.html#partial-mocking). 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:

```python
@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

```python
@patch('mymodule.date')
```

mocks out the entire `date` module as used within `mymodule`<sup id="fnref:where-to-patch" role="doc-noteref"><a href="#fn:where-to-patch" rel="footnote">1</a></sup> 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()`:

```python
    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

```python
    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:

```diff
 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:

```diff
 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! :tada:

The full test code now looks like this:

```python
# 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](https://pypi.org/project/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`):

```shell
$ pip install freezegun
$ pip freeze | grep i freezegun >> requirements.txt
```

Now, instead of

```python
    @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

```python
   @freeze_time('2006-08-24')
```

That’s wonderful. It’s just like magic.

![Spongebob with a rainbow above him and the words "it's magic"](https://peateasea.de/assets/images/its-magic-spongebob.jpg)
_Image credits: imgflip.com._

Our test code now reduces to this:

```python
# 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 :wink:).

## 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()`, and `time.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.:

```python
@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](https://github.com/spulec/freezegun/blob/master/README.rst).

## 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.

1. For more information about where to patch, see the [`mock` module’s “Where to patch” documentation](https://docs.python.org/3/library/unittest.mock.html#where-to-patch). [↩](#fnref:where-to-patch)