Python Time Manipulation with Freezegun
Introduction
Python provides a range of modules to work with dates, times, and timestamps. One of the most common tasks when working with time is manipulating the current time to simulate a future or past time for testing purposes. Freezegun is a third-party Python library that simplifies time manipulation by allowing developers to freeze the present time or specify a specific time for their tests. In this article, we will explore using Freezegun to manipulate time in Python.
Agenda
The article will cover the following topics:
- What is Freezegun?
- Installation and setup
- Basic usage and freezing the time
- Manipulating time for testing purposes
- Freezing time in tests using Pytest
- Timezone handling
- Test your Flask endpoint with Freezegun
- Write an integration test with Freezegun
- Best Practises for using FreezeGun
Freezegun
Freezegun is a Python library that allows you to manipulate the system clock in your tests. It can be used to simulate different points in time for testing purposes, making it easier to write time-sensitive code. In this article, we will explore how to use Freezegun and the best practices to follow.
Installation and setup
To install Freezegun, you can use pip:
pip install freezegun
Once you have installed Freezegun, you can import it into your Python code:
from freezegun import freeze_time
Basic usage and freezing the time
Freezegun allows you to freeze the system clock to a specific date and time. To do this, you can use the freeze_time
decorator:
from freezegun import freeze_time
import datetime
@freeze_time('2022-03-01')
def test_something():
assert datetime.datetime.now() == datetime.datetime(2022, 3, 1)
In this example, we are freezing the clock to March 1st, 2022, and then checking if datetime.datetime.now()
returns the expected value.
Manipulating time for testing purposes
Freezegun also allows you to manipulate the system clock for testing purposes. This can be useful when you need to test code that depends on the current time.
For example, let’s say you have a function that returns the current year:
import datetime
def get_current_year():
return datetime.datetime.now().year
To test this function using Freezegun, you can use the datetime.timedelta
class to manipulate the current time:
from freezegun import freeze_time
import datetime
@freeze_time('2022-03-01')
def test_get_current_year():
assert get_current_year() == 2022
@freeze_time('2022-12-31')
def test_get_current_year_end_of_year():
assert get_current_year() == 2022
In this example, we are testing the get_current_year
function with two different dates, March 1st, 2022 and December 31st, 2022.
Freezing time in tests using Pytest
Freezegun can also be used with Pytest, a popular Python testing framework. To use Freezegun with Pytest, you can install the pytest-freezegun
plugin:
pip install pytest-freezegun
Once you have installed the plugin, you can use the freeze_time
fixture to freeze the system clock:
import datetime
def test_something(freeze_time):
freeze_time('2022-03-01')
assert datetime.datetime.now() == datetime.datetime(2022, 3, 1)
In this example, we use the freeze_time
fixture to freeze the clock to March 1st, 2022.
Timezone handling
Freezegun also supports timezone handling. You can specify the timezone using the tz_offset
parameter:
from freezegun import freeze_time
import datetime
@freeze_time('2022-03-01', tz_offset=+5)
def test_timezone_handling():
assert datetime.datetime.now().strftime('%Z') == 'UTC+05:00'
In this example, we are freezing the clock to March 1st, 2022, and specifying a timezone offset of +5.
Post and Query Data at Specific Time from Flask API
Here’s an example of how to use Freezegun to schedule posts to a Flask endpoint at different times and then query the data from a database:
from flask import Flask, jsonify, request
from freezegun import freeze_time
import datetime
import sqlite3
app = Flask(__name__)
# Connect to the database
conn = sqlite3.connect('example.db')
c = conn.cursor()
# Create a table to store the posts
c.execute('''CREATE TABLE IF NOT EXISTS posts
(id INTEGER PRIMARY KEY AUTOINCREMENT,
payload TEXT,
created_at TIMESTAMP)''')
conn.commit()
# Define a function to insert a post into the database
def insert_post(payload):
now = datetime.datetime.now()
c.execute("INSERT INTO posts (payload, created_at) VALUES (?, ?)", (payload, now))
conn.commit()
# Define a route to accept POST requests and insert the payload into the database
@app.route('/post', methods=['POST'])
def post():
payload = request.json['payload']
insert_post(payload)
return jsonify({'message': 'Post created successfully'})
# Define a route to query the posts from the database
@app.route('/posts', methods=['GET'])
def get_posts():
with app.app_context():
c.execute("SELECT * FROM posts")
rows = c.fetchall()
result = [{'id': row[0], 'payload': row[1], 'created_at': row[2]} for row in rows]
return jsonify(result)
# Schedule three posts to be sent at different times
@freeze_time('2022-01-01')
def schedule_post_1():
payload = {'payload': 'January payload'}
with app.test_client() as client:
client.post('/post', json=payload)
@freeze_time('2022-02-01')
def schedule_post_2():
payload = {'payload': 'February payload'}
with app.test_client() as client:
client.post('/post', json=payload)
@freeze_time('2022-03-01')
def schedule_post_3():
payload = {'payload': 'March payload'}
with app.test_client() as client:
client.post('/post', json=payload)
# Run the scheduled posts
schedule_post_1()
schedule_post_2()
schedule_post_3()
# Query the posts at different times
with freeze_time('2022-01-01'):
with app.app_context():
print(get_posts().get_json())
with freeze_time('2022-02-15'):
with app.app_context():
print(get_posts().get_json())
with freeze_time('2022-03-30'):
with app.app_context():
print(get_posts().get_json())
if __name__ == '__main__':
app.run()
In this example, we define a Flask app with two routes: one to accept POST requests and insert the payload into a database and another to query the data from the database. We use SQLite as the database engine.
We also define three functions to schedule posts to the Flask endpoint at different times, using Freezegun to manipulate the current time. Finally, we query the posts from the database at other times using the get_posts()
function and freeze_time.
The schedule_post_*
functions are decorated with @freeze_time
to set the current time to a specific date before making the POST request to the /post
endpoint.
Postgresql
If you want to use a PostgreSQL database instead, you can use the following code:
from flask import Flask, jsonify, request
from freezegun import freeze_time
import datetime
import psycopg2
app = Flask(__name__)
# Connect to the database
conn = psycopg2.connect(
dbname="test_db",
user="test_user",
password="test_password",
host="test_host",
port="5432"
)
c = conn.cursor()
# Create a table to store the posts
c.execute('''CREATE TABLE IF NOT EXISTS posts
(id SERIAL PRIMARY KEY,
payload TEXT,
created_at TIMESTAMP)''')
conn.commit()
# Define a function to insert a post into the database
def insert_post(payload):
now = datetime.datetime.now()
c.execute("INSERT INTO posts (payload, created_at) VALUES (%s, %s)", (payload, now))
conn.commit()
# Define a route to accept POST requests and insert the payload into the database
@app.route('/post', methods=['POST'])
def post():
payload = request.json['payload']
insert_post(payload)
return jsonify({'message': 'Post created successfully'})
# Define a route to query the posts from the database
@app.route('/posts', methods=['GET'])
def get_posts():
c.execute("SELECT * FROM posts")
rows = c.fetchall()
result = [{'id': row[0], 'payload': row[1], 'created_at': row[2]} for row in rows]
return jsonify(result)
# Schedule three posts to be sent at different times
@freeze_time('2022-01-01')
def schedule_post_1():
payload = {'payload': 'January payload'}
with app.test_client() as client:
client.post('/post', json=payload)
@freeze_time('2022-02-01')
def schedule_post_2():
payload = {'payload': 'February payload'}
with app.test_client() as client:
client.post('/post', json=payload)
@freeze_time('2022-03-01')
def schedule_post_3():
payload = {'payload': 'March payload'}
with app.test_client() as client:
client.post('/post', json=payload)
# Run the scheduled posts
schedule_post_1()
schedule_post_2()
schedule_post_3()
# Query the posts at different times
with freeze_time('2022-01-01'):
print(get_posts().get_json())
with freeze_time('2022-02-15'):
print(get_posts().get_json())
with freeze_time('2022-03-30'):
print(get_posts().get_json())
if __name__ == '__main__':
app.run()
To call the post
endpoint multiple times at a specific time, you can use the freeze_time
decorator from freezegun
to freeze the current time before each call. Here's an example of how to do that:
import datetime
from freezegun import freeze_time
# Define a function to call the post endpoint with a given payload
def call_post(payload):
with app.test_client() as client:
client.post('/post', json={'payload': payload})
# Freeze the time and call the post endpoint three times
with freeze_time('2022-01-01'):
call_post('January payload')
with freeze_time('2022-02-01'):
call_post('February payload')
with freeze_time('2022-03-01'):
call_post('March payload')
To call the get_posts
endpoint at certain times within the same test function, you can call the function with app.test_client()
and check the response. Here's an example of how to do that:
# Query the posts at different times
with freeze_time('2022-01-01'):
response = app.test_client().get('/posts')
assert response.status_code == 200
assert len(response.get_json()) == 1
with freeze_time('2022-02-15'):
response = app.test_client().get('/posts')
assert response.status_code == 200
assert len(response.get_json()) == 2
with freeze_time('2022-03-30'):
response = app.test_client().get('/posts')
assert response.status_code == 200
assert len(response.get_json()) == 3
This will call the get_posts
endpoint at different times and check that the expected number of posts is returned.
Integration test
Let’s create an integration test using Python’s unittest
framework to call the get_posts()
and post()
methods at different times:
import unittest
from app import app, get_posts, insert_post
from freezegun import freeze_time
class TestIntegration(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
self.conn = sqlite3.connect('example.db')
self.c = self.conn.cursor()
def tearDown(self):
self.conn.close()
@freeze_time('2022-01-01')
def test_schedule_posts_and_get(self):
# Schedule three posts to be sent at different times
payload1 = {'payload': 'January payload'}
self.app.post('/post', json=payload1)
payload2 = {'payload': 'February payload'}
with freeze_time('2022-02-01'):
self.app.post('/post', json=payload2)
payload3 = {'payload': 'March payload'}
with freeze_time('2022-03-01'):
self.app.post('/post', json=payload3)
# Query the posts at different times
with freeze_time('2022-01-01'):
response1 = self.app.get('/posts')
self.assertEqual(response1.status_code, 200)
self.assertEqual(len(response1.json), 1)
with freeze_time('2022-02-15'):
response2 = self.app.get('/posts')
self.assertEqual(response2.status_code, 200)
self.assertEqual(len(response2.json), 2)
with freeze_time('2022-03-30'):
response3 = self.app.get('/posts')
self.assertEqual(response3.status_code, 200)
self.assertEqual(len(response3.json), 3)
if __name__ == '__main__':
unittest.main()
This test sets up the Flask app and database connection in the setUp()
method and tears them down in the tearDown()
method. It then defines a single test method test_schedule_posts_and_get()
that schedules three posts at different times and queries the database using the get_posts()
method at the same time. It uses the freeze_time
decorator to set the time to a specific value during each test case. It also uses the assertEqual()
method to check that the response status code is 200 and that the number of posts returned is as expected. Finally, it runs the test using unittest.main()
.
All the code for this is available on the below Github repository:
Best practices when using Freezegun
When using Freezegun, there are a few best practices to follow:
- Don’t overuse Freezegun: While Freezegun can be extremely useful in testing, it is essential not to overuse it. If you find yourself using Freezegun frequently, it may be a sign that your code is too tightly coupled to time and could benefit from being refactored.
- Use the correct timezone: If your application needs to handle timezones, set the correct timezone when using Freezegun. This will ensure that your tests are accurate and your application behaves correctly.
- Keep time manipulation localized: When using Freezegun, it is essential to keep time manipulation localized to the tests where it is needed. Avoid modifying the system time outside of tests, as this can have unexpected consequences and make it harder to reason about your code.
- Keep tests deterministic: When using Freezegun, keep your tests deterministic. This means that the results of your tests should be the same each time they are run, regardless of the system time. If your tests are non-deterministic, debugging and fixing issues can be difficult.
- Test edge cases: Be sure to test edge cases when using Freezegun. For example, test how your code handles leap years, daylight saving time changes, and other unusual circumstances.
Conclusion
Manipulating time in Python can be challenging, but Freezegun simplifies the process and enables developers to focus on testing their code. Freezegun allows for a flexible and straightforward approach to manipulating time that can be useful in various testing scenarios. Whether you’re testing time-sensitive code, working with timezones, or need to simulate specific times, Freezegun provides an excellent solution that saves time and reduces errors.
That’s it for this article! Feel free to leave feedback or questions in the comments. If you found this an exciting read, leave some claps and follow! Cheers!