Mocking Python datetime In Tests With FreezeGun
Tags: beginners, python, 100daysofpython
Published: 2021-09-07 10:15:13

_**I write content for AWS, Kubernetes, Python, JavaScript and more. To view all the latest content, be sure to [visit my blog](https://blog.dennisokeeffe.com) and subscribe to my newsletter. [Follow me on Twitter](https://twitter.com/dennisokeeffe92).**_

> This is Day 17 of the #100DaysOfPython challenge.

This post will use the [FreezeGun](https://github.com/spulec/freezegun) library to demonstrate how to mock the `datetime.datetime.now` return value to set up consistent test environments.

The repo code can be found on GitHub at [`okeeffed/hello-python-datetime`](https://github.com/okeeffed/hello-python-datetime)

## Prerequisites

1. Familiarity with [Pipenv](https://pipenv.pypa.io/en/latest/). See [here](https://blog.dennisokeeffe.com/blog/2020-07-04-abcs-of-pipenv) for my post on Pipenv.
2. Familiarity with [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/gettingstarted/install.html). See [here](https://blog.dennisokeeffe.com/blog/2021-07-19-hello-jupyter-notebook) for my post on JupyterLab.
3. My previous blog post [Datetime In Python](https://blog.dennisokeeffe.com/blog/2021-08-04-datetime-in-python.mdx)
4. Familiarity with [PyTest](https://docs.pytest.org/en/latest/). See my blog post [Python Unit Testing With PyTest](https://blog.dennisokeeffe.com/blog/2021-07-26-python-unit-testing-with-pytest) for a start post.

## Getting started

The previous code will built on top of the code from [Datetime In Python](https://blog.dennisokeeffe.com/blog/2021-08-04-datetime-in-python.mdx). The final repo is at [okeeffed/hello-python-datetime](https://github.com/okeeffed/hello-python-datetime).

For the sake of simplicity, we will operate as if we are building a brand new repo:

Let's create the `hello-python-datetime` directory and install the required dependencies.

```s
# Make the `hello-python-datetimes` directory
$ mkdir hello-python-datetimes
$ cd hello-python-datetimes

# Init the virtual environment
$ pipenv --three
$ pipenv install --dev ipython freezegun types-freezegun

# Create a folder to place files
$ mkdir src tests
# Create the required files
$ touch src/datetimes.py src/__init__.py tests/datetimes_test.py tests/__init__.py main.py
```

At this stage, we are now ready to update our `main.py` file and `src/datetimes.py` to be up to par with what we need for testing.

Add the following to `src/datetimes.py`:

```py
from datetime import date


def is_date_before_today(date_str: str):
    """Check if date is before today

    Args:
        date_str (str): String of a date to pass

    Returns:
        bool: Value of if date is before today
    """
    try:
        date_obj = date.fromisoformat(date_str)
        return date_obj < date.today()
    except Exception:
        return False
```

Add the following to `main.py`:

```py
from src.datetimes import is_date_before_today
from datetime import datetime, timedelta

print(is_date_before_today("2019-01-01"))
print(is_date_before_today("2022-01-01"))
print(is_date_before_today("2021-08-03"))
print(is_date_before_today("2021-08-04"))

now = datetime.now()
now_str = now.strftime('%Y-%m-%d')
print(now_str)
print(is_date_before_today(now_str))

now_subtract_one_day = now - timedelta(days=2)

now_subtract_one_day_str = now_subtract_one_day.strftime('%Y-%m-%d')
print(now_subtract_one_day_str)
print(is_date_before_today(now_subtract_one_day_str))

now_add_one_day = now + timedelta(days=1)

now_add_one_day_str = now_add_one_day.strftime('%Y-%m-%d')
print(now_add_one_day_str)
print(is_date_before_today(now_add_one_day_str))
```

Running `python main.py` should bring us up to par with the following:

```s
$ python main.py
True
False
True
True
2021-08-05
False
2021-08-03
True
2021-08-06
False
```

The output matches up to us printing values out from `main.py`. We are at a stage now where we are up to par and able to start writing tests.

> Note: If you are using a virtual environment, you will need to run `pipenv shell` to enter the virtual environment.

## Exploring FreezeGun with PyTest

[FreezeGun](https://github.com/spulec/freezegun) is a library that helps with mocking out the `datetime.datetime.now` function. It is a very useful tool for testing code that uses the `datetime` library.

We can use the library with a decorator for the test or creating a `with` block.

To demonstrate, add the following code to `tests/datetimes_test.py`:

```py
import datetime
from freezegun import freeze_time
from src.datetimes import is_date_before_today


def test_freeze_time():
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)
    # Mocking the time to be 2012-01-14
    with freeze_time("2012-01-14"):
        assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
    # Without the mock, the time should be back to normal
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)


@freeze_time("2012-01-14")
def test_freeze_time_with_decorator():
    # Testing with a decorator that mocks throughout the test
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
```

The first test demonstrates the `with` block will the second test demonstrates usage with a decorator.

Running `pipenv run pytest` will now run the tests and display the results.

```s
$ pipenv run pytest
pipenv run pytest
================================== test session starts ===================================
platform darwin -- Python 3.9.6, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /path/to/hello-python-datetimes
collected 2 items

tests/test_datetimes.py ..                                                          [100%]

=================================== 2 passed in 0.21s ====================================
```

Now we are ready to test our `is_date_before_today` function in a manner similar to how our `main.py` invokes the functions.

## Testing the `is_date_before_today` function

Let's update our `tests/datetimes_test.py` file to test the `is_date_before_today` function.

```py
import datetime
from freezegun import freeze_time
from src.datetimes import is_date_before_today


def test_freeze_time():
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)
    # Mocking the time to be 2012-01-14
    with freeze_time("2012-01-14"):
        assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
    # Without the mock, the time should be back to normal
    assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)


@freeze_time("2012-01-14")
def test_freeze_time_with_decorator():
    # Testing with a decorator that mocks throughout the test
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)

# Converting the output we expected from main.py into a set of tests.
# Mocking time unnecessary, but done for the sake of completion.


@freeze_time("2021-08-05")
def test_is_date_before_today():
    """Should return False"""
    now = datetime.datetime.now()
    now_str = now.strftime('%Y-%m-%d')
    assert is_date_before_today(now_str) is False


@freeze_time("2021-08-05")
def test_is_one_day_ago_before_today():
    """Should return True"""
    now_subtract_one_day = datetime.datetime.now() - datetime.timedelta(days=1)
    now_subtract_one_day_str = now_subtract_one_day.strftime('%Y-%m-%d')
    assert is_date_before_today(now_subtract_one_day_str) is True


@freeze_time("2021-08-05")
def test_is_one_day_ahead_before_today():
    """Should return False"""
    now_add_one_day = datetime.datetime.now() + datetime.timedelta(days=1)
    now_add_one_day_str = now_add_one_day.strftime('%Y-%m-%d')
    assert is_date_before_today(now_add_one_day_str) is False
```

In our test, we are freezing time (using the decorator) to the date of this blog post `2021-08-05` checking the following scenarios:

1. `is_date_before_today` when compared to today should be `False`.
2. `is_date_before_today` when compared to one day ago should be `True`.
3. `is_date_before_today` when compared to one day ahead should be `False`.

We can confirm this to be true by once again running `pipenv run pytest`:

```s
$ pipenv run pytest
================================== test session starts ===================================
platform darwin -- Python 3.9.6, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/dennisokeeffe/code/blog-projects/hello-python-datetimes
collected 5 items

tests/test_datetimes.py .....                                                      [100%]

=================================== 5 passed in 0.21s ====================================
```

## Summary

Today's post demonstrated how to use the [FreezeGun](https://github.com/spulec/freezegun) package to mock the date for testing with PyTest.

## Resources and further reading

1. [The ABCs of Pipenv](https://blog.dennisokeeffe.com/blog/2020-07-04-abcs-of-pipenv)
2. [Hello, JupyterLab](https://blog.dennisokeeffe.com/blog/2021-07-19-hello-jupyter-notebook)
3. [FreezeGun](https://github.com/spulec/freezegun)
4. [Pipenv](https://pipenv.pypa.io/en/latest/)
5. [Python Unit Testing With PyTest](https://blog.dennisokeeffe.com/blog/2021-07-26-python-unit-testing-with-pytest)

_Photo credit: [`pawel_czerwinski`](https://unsplash.com/@pawel_czerwinski)_


_Originally posted on my [blog](https://blog.dennisokeeffe.com/blog/2021-08-05-mocking-python-datetime-in-tests-with-freezegun). To see new posts without delay, read the posts there and subscribe to my newsletter._