Back to home

PrerequisitesGetting startedExploring FreezeGun with PyTestTesting the `is_date_before_today` functionSummaryResources and further reading
Mocking Python datetime In Tests With FreezeGun main image

Mocking Python datetime In Tests With FreezeGun

This post will use the 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

Prerequisites

  1. Familiarity with Pipenv. See here for my post on Pipenv.
  2. Familiarity with JupyterLab. See here for my post on JupyterLab.
  3. My previous blog post Datetime In Python
  4. Familiarity with PyTest. See my blog post 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. The final repo is at 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.

1 2 3 4 5 6 7 8 9 10 11 12 # 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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:

1 2 3 4 5 6 7 8 9 10 11 $ 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.

Exploring FreezeGun with PyTest

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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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.

1 2 3 4 5 6 7 8 9 10 $ 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.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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:

1 2 3 4 5 6 7 8 9 $ 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 package to mock the date for testing with PyTest.

Resources and further reading

Photo credit: pawel_czerwinski

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

Related articles


1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.