A Simple Guide To Controlling Time in Pytest Using Freezegun

With time-dependent code, a key challenge arises in testing it across various time and dates.

Consider a scenario where you’re working on a global transaction application. This application involves several time-dependent activities such as recording transaction time, date, the timezone of the transaction, and more.

How do you ensure your application functions correctly when a transaction occurs outside your country?

One approach might be to manually adjust the time, date, and timezone of your system. However, this method is non-deterministic, time-consuming, and impractical for running individual tests.

So, what’s the solution?

You freeze time!

This article introduces the exciting library called Freezegun, offering a streamlined way to test your time-dependent code.

Freezegun is a Python library designed to freeze time for specific test functions or blocks of code.

It provides a convenient and professional solution, allowing you to isolate and control time-related aspects of your code without the need for extensive environment setup.

In this comprehensive guide, you’ll thoroughly explore the Freezegun library. We’ll start with how to seamlessly integrate Freezegun into your Python code and unit tests.

Through engaging examples, we’ll demonstrate the library’s functionality, allowing you to gain a deep understanding of how Freezegun works.

As we progress, we’ll delve into advanced uses of Freezegun, uncovering its capabilities beyond the basics.

Lastly, we’ll share best practices and valuable tips for using Freezegun in a professional context.

By the end of this guide, you’ll be well-equipped to harness the full power of Freezegun in your projects and control time across your testing suite and time dependent applications.

Example Code

What You’ll Learn

By the end of this tutorial, you will:

  • Clearly understand the concept of time manipulation in testing
  • Gain brief insights about the Freezegun Python library and how to use it to your advantage
  • How to test time-dependent code with freezegun

The Need For Time Manipulation in Testing

Time manipulation in testing provides you the capability to exert control over and manipulate time during the evaluation of time-dependent code.

Consider, for example, a financial application that frequently involves numerous time-dependent activities, including recording transaction dates, account opening dates, transaction times, and more.

When testing such an application, a significant challenge arises in testing it across diverse time and date scenarios without interfering with the current system date and time automatically recorded during tests.

The ability to manipulate time addresses these challengees by allowing you to run tests using customized date and time settings.

This ensures a comprehensive evaluation of your application across timezones, validating its seamless functionality under various time-related conditions.

What is Freezegun?

Freezegun is a Python library designed to freeze time during code execution, great for testing time-dependent functionality.

Its primary utility lies in mocking the current time returned by Python’s datetime.datetime.now() and related functions.

By freezing time, Freezegun enables you to simulate various points in time without affecting the actual advancement of the system clock.

This is beneficial when writing unit tests that include date and time calculations.

By providing a consistent and reproducible environment, Freezegun ensures that your tests produce reliable results, regardless of when or where they are executed.

This capability simplifies the testing process, eliminating the need to manipulate the system clock and facilitates the development of more deterministic and robust test suites.

How To Install Freezegun?

You can install Freezegun using pip,

1
pip install freezegun

Alternatively, stick it into your requirements.txt file and run pip install -r requirements.txt to install it.

Using Freezegun with Pytest

Using Freezegun in your code is just like importing any other Py module. The module of interest is freezegun.freeze_time().

1
from freezegun import freeze_time

Here is a straightforward example that prints greetings based on the current time,

tests/test_simple_python_freezegun.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
25
26
27
28
29
30
31
32
33
34
35
36
import pytest
from freezegun import freeze_time
from datetime import datetime


def get_greeting():
current_time = datetime.now()
if current_time.hour < 12:
return "Good morning!"
elif 12 <= current_time.hour < 18:
return "Good afternoon!"
else:
return "Good evening!"


# Test case without freezing time
def test_get_greeting_default():
# This test will depend on the actual time when the test is run
greeting = get_greeting()
assert greeting in ["Good morning!", "Good afternoon!", "Good evening!"]


# Test case with frozen time using freezegun
@freeze_time("2023-01-01 12:00:00")
def test_get_greeting_frozen_time():
# This test will always use the frozen time, so the result is predictable
greeting = get_greeting()
assert greeting == "Good afternoon!"


# Another test case with a different frozen time
@freeze_time("2023-01-01 20:00:00")
def test_get_greeting_frozen_time_evening():
greeting = get_greeting()
assert greeting == "Good evening!"

In the example above, there’s a function named get_greeting() that sends a greeting message - either “Good morning!”, “Good afternoon!”, or “Good evening!” - based on the current time.

It’s worth noting that we have three distinct test functions, each utilizing the freeze_time feature.

When running the tests, you can expect the following results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ pytest -v
================================================= test session starts =================================================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 -- C:\Users\PythonTesting\AppData\Local\Programs\Python\Python311\python.exe
cachedir: .pytest_cache
rootdir: G:\Pytest-with-Eric\python-freezegun-example
configfile: pytest.ini
plugins: cov-4.1.0
collected 3 items

tests/test_simple_python_freezegun.py::test_get_greeting_default PASSED [ 33%]
tests/test_simple_python_freezegun.py::test_get_greeting_frozen_time PASSED [ 66%]
tests/test_simple_python_freezegun.py::test_get_greeting_frozen_time_evening PASSED [100%]

================================================== 3 passed in 0.22s ==================================================

Having looked at a trivial example, let’s move onto a more practical application of Freezegun. But first, let’s set up our project.

Project Setup

Let’s organize our project to demonstrate executing tests using different Python testing frameworks.

Prerequisites

To follow this guide, you should have:

  • Python 3.11+ installed

Getting Started

Our example code repo looks like this. We’ll explain what each file does and why it’s used.

1
2
3
4
5
6
7
8
├── .gitignore
├── README.md
├── requirements.txt
├───src
│ └── database_manager.py
└───tests
├── test_database_manager.py
└── test_simple_python_freezegun.py

To get started. clone the Github Repo here, or you can create your own repo by creating a folder and running git init to initialize it.

Make sure to create a virtual environment and install the required packages from the requirements.txt file.

1
pip install -r requirements.txt

Real-World Example and Case Studies

Example Code

Our example code is a straightforward Object-Relational Mapping (ORM) based database manager that contains various functionalities for managing databases.

src/database_manager.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from sqlalchemy import create_engine, Column, Integer, String, Sequence
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker

def create_database():
"""
Creating an SQLite database in memory for this example
"""
engine = create_engine('sqlite:///:memory:', echo=False)
return engine

def create_user_table(engine):
"""
Define a simple User model
"""
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
name = Column(String(50))
date_time = Column(String(50))

# Create the table in the database
Base.metadata.create_all(engine)

return User

def add_user(session, user):
"""
Add a new user to the database
"""
session.add(user)
session.commit()

def query_user(session, User, name):
"""
Query the database to retrieve the user
"""
queried_user = session.query(User).filter_by(name=name).first()
return queried_user

The provided example code comprises four distinct functions: create_database(), create_user_table(), add_user(), and query_user(). Their respective purposes are outlined in the code documentation.

Test Code

It’s time to perform tests.

Our test code is as follows,

test/test_database_manager.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pytest
from freezegun import freeze_time
from sqlalchemy.orm import sessionmaker
from datetime import datetime
from src.database_manager import (
create_database,
create_user_table,
add_user,
query_user,
)


# Fixture to setup test environment
@pytest.fixture
def setup_environment():
engine = create_database()
Session = sessionmaker(bind=engine)
session = Session()
yield engine, session


# Fixture to create table
@pytest.fixture
def create_table(setup_environment):
engine = setup_environment[0]
User = create_user_table(engine)
yield User


# Testing adding and quering data
@freeze_time("2023-12-14 23:31:00")
def test_create_user(setup_environment, create_table):
session = setup_environment[1]
User = create_table
# Adding a new user to database
new_user = User(name="John Doe", date_time=datetime.now())
if add_user(session, new_user):
pass
# Checking if the data added successfully
queried_user = query_user(session, User, "John Doe").date_time
assert queried_user == "2023-12-14 23:31:00"

The test code incorporates two fixtures: setup_environment() and create_table(). The sole test function is named test_create_user(), and within this function, we employ the freeze_time feature.

We assert that the queried user’s date and time is equal to the frozen time.

Running Test

Running the test will display the following results,

1
2
3
4
5
6
7
8
9
10
11
12
$ pytest -v
================================================= test session starts =================================================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 -- C:\Users\PythonTesting\AppData\Local\Programs\Python\Python311\python.exe
cachedir: .pytest_cache
rootdir: G:\Pytest-with-Eric\python-freezegun-example
configfile: pytest.ini
plugins: cov-4.1.0
collected 1 item

tests/test_database_manager.py::test_create_user PASSED [100%]

================================================== 1 passed in 1.07s ==================================================

Pytest Freezegun Plugin

Freezegun also comes with a Pytest plugin that allows you to use Freezegun with Pytest.

To use the Freezegun plugin, you need to install it using pip,

1
pip install pytest-freezegun

Once installed, you can freeze time using the freezer fixture.

1
2
3
4
5
def test_frozen_date(freezer):
now = datetime.now()
time.sleep(1)
later = datetime.now()
assert now == later

You can do a bunch of cool stuff with time as documented here.

Handling Timezones in Freezegun

What if your colleagues are in Europe and you’re in the US?

How do you ensure you get consistent testing results when running these unit tests?

Maybe your code needs to work differently for different timezones.

Freezegun allows you to also handle time zones. The following example demonstrates handling time zones in Freezegun.

1
2
3
4
5
6
7
8
9
10
11
12
from freezegun import freeze_time
import datetime

## Freezing time with timezone -4
@freeze_time("2012-01-14 03:21:34", tz_offset=-4)
def test_timezone():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 13, 23, 21, 34)

## Freezing time with timezone +6
@freeze_time("2012-01-14 03:21:34", tz_offset=+6)
def test_timezone_2():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14, 9, 21, 34)

In the example above, tz_offset defines the timezone. We set two different time zones for the functions test_timezone() and test_timezone_2().

Freezegun with Pytest Fixtures

Pytest fixtures are a powerful feature of the Pytest framework for Python, used for setting up and tearing down test environments.

The following example demonstrates how you can use the Freezegun with Pytest Fixture,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest
from freezegun import freeze_time
from datetime import datetime

# Define a fixture to freeze time for the tests
@pytest.fixture
def frozen_time():
with freeze_time("2023-01-01 12:00:00"):
yield

# Test function that uses the frozen_time fixture
def test_example(frozen_time):
# Your time-sensitive code here
current_time = datetime.now()
assert current_time == datetime(2023, 1, 1, 12, 0, 0)

In this example, the frozen_time() fixture employs Freezegun to manipulate the time for the test function test_example() and force it to use the custom time instead of using the system time.

You’ll have the below output:

1
2
3
4
5
6
7
8
9
10
11
================================================= test session starts =================================================
platform win32 -- Python 3.12.0, pytest-7.4.0, pluggy-1.2.0 -- C:\Users\PythonTesting\AppData\Local\Programs\Python\Python311\python.exe
cachedir: .pytest_cache
rootdir: G:\Pytest-with-Eric\python-freezegun-example
configfile: pytest.ini
plugins: cov-4.1.0
collected 1 item

tests/test_freezegun_with_fixture.py::test_freezegun_fixture PASSED [100%]

================================================== 1 passed in 0.33s ==================================================

Best Practices for Using Freezegun

Let’s dive into some tips and tricks that help you to use Freezegun efficiently.

Avoid overusing Freezegun
While Freezegun is a powerful tool for time-dependent testing, don’t overuse it. If you’re using Freezegun frequently, it may be a sign that your code is too tightly coupled to time and could benefit from a refactor.

Use context managers to control specific blocks of code
Freezegun comes with a context manager, allowing you to freeze time for a specific block of code as opposed to freezing the whole test via a decorator.

1
2
3
4
5
6
7
8
from freezegun import freeze_time

def test_example():
# Your non-time-sensitive code here

with freeze_time("2023-01-01 12:00:00"):
# Your time-dependent code here

instead of

1
2
3
4
5
from freezegun import freeze_time

@freeze_time("2023-01-01 12:00:00")
def test_example():
# Your time-sensitive code here

Write deterministic tests
Keep your tests deterministic when using Freezegun. This means your test results should be the same each time they run. Otherwise you may end up with flaky tests.

Keep time manipulation localized
It is essential to keep time manipulation localized to the tests where it is needed. Avoid modifying the system time outside your tests, as this can lead to unexpected results.

Cleaning after tests
When employing the Freezegun object, it is imperative to perform cleanup after executing tests. Always use the stop() method or the context manager to conclude the usage of Freezegun. This will unfreeze the time, helping you to avoid interfering with other tests or parts of your code.

Or if using fixtures to freeze time, you can use the yield keyword to perform teardown after the tests.

1
2
3
4
5
6
7
from freezegun import freeze_time

def test_example():
freezer = freeze_time("2023-01-01 12:00:00")
freezer.start()
# Your time-sensitive code here
freezer.stop()

Final Thoughts and Conclusion

That’s all for now.

In this article, you explored a brief overview of the need for time manipulation in testing and how the Freezegun library can help you elegantly achieve this.

You also learnt how to use Freezegun with Pytest using a simple greeting example and a more complex ORM example.

Lastly, you learnt some of the best practices and tips in time manipulation and how to use Freezegun efficiently.

Freezegun is a handy tool for writing reliable and deterministic tests for code that involves time and date, simplifying the need to manipulate the system time.

Take this, apply it to your own projects to write more robust and reliable tests.

If you have any ideas for improvement or like me to cover any topics please comment below or send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Reading

Example Code Repo
A Step-by-Step Guide To Using Pytest Fixtures With Arguments
What Is Pytest Caplog? (Everything You Need To Know)
How To Use Pytest Logging And Print To Console And File (A Comprehensive Guide)
Introduction to Pytest Mocking - What It Is and Why You Need It
freezegun 0.3.4
Freezegun
Python Time Manipulation with Freezegun
Mocking Python datetime in Tests with FreezeGun
How do I set or freeze time in python?
how to use freezegun on logbook