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.
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
36import 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
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
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
14pytest -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
40from 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
42import 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
def setup_environment():
engine = create_database()
Session = sessionmaker(bind=engine)
session = Session()
yield engine, session
# Fixture to create table
def create_table(setup_environment):
engine = setup_environment[0]
User = create_user_table(engine)
yield User
# Testing adding and quering data
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
12pytest -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
5def 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
12from freezegun import freeze_time
import datetime
## Freezing time with timezone -4
def test_timezone():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 13, 23, 21, 34)
## Freezing time with timezone +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
15import pytest
from freezegun import freeze_time
from datetime import datetime
# Define a fixture to freeze time for the tests
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
8from 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 of1
2
3
4
5from freezegun import freeze_time
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
7from 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