
pytest + FreezeGunで日時モックを使ったテストコードを書く
はじめに
データアナリティクス事業本部のkobayashiです。
Pythonで日時に関するテストを書く際に、現在時刻や特定の日時に依存する処理のテストは通常困難です。FreezeGunライブラリを使用することで、テスト実行時の時刻を任意の値に固定でき、時間に依存する処理を安定してテストできるようになります。今回はpytestでFreezeGunを使った日時のテスト方法をまとめます。
FreezeGunとは
FreezeGunは、Pythonのdatetimeモジュールやtimeモジュールをモック化して、テスト実行時の時刻を任意の値に固定できるライブラリです。時間に依存する処理のテストを書く際に、実行時刻に関係なく一貫した結果を得ることができます。
FreezeGunを使用することで、以下のような利点があります:
- 時間に依存する処理のテストが安定する
- 特定の日時での処理をテストできる
- 時間の経過をシミュレートできる
FreezeGunを使ってみる
環境
- Python: 3.12
- pytest: 7.4.3
- freezegun: 1.5.2
インストール
FreezeGunはpipでインストールできます。
pip install freezegun
テスト対象の関数
今回テストする関数は現在の日時を取得して文字列で返すシンプルな関数です。
from datetime import datetime
def get_now_dt():
now = datetime.now()
return now.strftime('%Y-%m-%d')
この関数は実行時の現在時刻に依存するため、通常のテストでは実行日によって結果が変わってしまいます。
FreezeGunを使ったテスト
FreezeGunを使用することで、テスト実行時の日時を固定できます。
from freezegun import freeze_time
from main import get_now_dt
@freeze_time('2025-06-01')
def test_get_now_dt():
ret = get_now_dt()
assert ret == '2025-06-01'
@freeze_time
デコレータを使用することで、テスト関数内のdatetime.now()
の実行結果を指定した日時(2025-06-01
)に固定できます。
$ pytest -v
...
test_main.py::test_get_now_dt PASSED [100%]
コンテキストマネージャーを使った方法
デコレータ以外にも、コンテキストマネージャーとしてFreezeGunを使用することもできます。
@pytest.mark.parametrize(
["now_dt"],
[
pytest.param("2025-04-02"),
pytest.param("2025-05-02"),
pytest.param("2025-06-02"),
],
)
def test_get_now_dt_context(now_dt):
with freeze_time(now_dt):
ret = get_now_dt()
assert ret == datetime.strptime(now_dt, "%Y-%m-%d")
# コンテキストマネージャーを抜けると通常の時刻に戻る
actual_now = datetime.now()
print(f"実際の現在時刻: {actual_now}")
コンテキストマネージャーを使用すると、with
文のブロック内でのみ時刻が固定され、ブロックを抜けると通常の時刻に戻ります。
$ pytest -v
...
test_main.py::test_get_now_dt_context[('2025-04-02',)-()-None] PASSED [ 33%]実際の現在時刻: 2025-06-27 09:21:46.368555
test_main.py::test_get_now_dt_context[('2025-05-02',)-()-None] PASSED [ 66%]実際の現在時刻: 2025-06-27 09:21:46.375837
test_main.py::test_get_now_dt_context[('2025-06-02',)-()-None] PASSED [100%]実際の現在時刻: 2025-06-27 09:21:46.382808
デコレータとコンテキストマネージャーを組み合わせた方法
デコレータとコンテキストマネージャーを組み合わせてみます。
@pytest.mark.parametrize(
["now_dt"],
[
pytest.param("2025-04-03"),
pytest.param("2025-05-03"),
pytest.param("2025-06-03"),
],
)
@freeze_time("2025-06-01")
def test_get_now_dt_context2(now_dt):
with freeze_time(now_dt):
ret = get_now_dt()
assert ret == datetime.strptime(now_dt, "%Y-%m-%d")
# コンテキストマネージャーを抜けると通常の時刻に戻る
ret = get_now_dt()
assert ret == datetime.strptime("2025-06-01", "%Y-%m-%d")
$ pytest -v
...
test_main.py::test_get_now_dt_context[('2025-04-03',)-()-None] PASSED [ 33%]
test_main.py::test_get_now_dt_context[('2025-05-03',)-()-None] PASSED [ 66%]
test_main.py::test_get_now_dt_context[('2025-06-03',)-()-None] PASSED [100%]
このようにデコレータとコンテキストでfreeze_time
を組み合わせることで、様々な日時パターンを効率的にテストできます。
時刻を含む詳細なテスト
日付だけでなく時刻も含めた詳細なテストも可能です。
@freeze_time('2025-06-05 14:30:00')
def test_get_now_datetime_with_time():
ret = get_now_dt()
assert ret.year == 2025
assert ret.month == 6
assert ret.day == 5
assert ret.hour == 14
assert ret.minute == 30
assert ret.second == 0
$ pytest -v
...
test_main.py::test_get_now_datetime_with_time PASSED [100%]
時刻を指定することでより詳細な時刻を使ったテストも可能です。
タイムゾーンを考慮したテスト
FreezeGunはタイムゾーンも考慮したテストが可能です。
from datetime import datetime
import pytz
def get_now_with_timezone(timezone):
tz = pytz.timezone(timezone)
now = datetime.now(tz)
return now
@freeze_time("2025-06-10 14:30:00", tz_offset=0)
@pytest.mark.parametrize(
["tz", "day", "hour", "minute"],
[
pytest.param("Asia/Tokyo", 10, 23, 30),
pytest.param("America/Los_Angeles", 10, 7, 30),
pytest.param("UTC", 10, 14, 30),
],
)
def test_get_now_with_timezone(tz, day, hour, minute):
ret = get_now_with_timezone(tz)
assert ret.day == day
assert ret.hour == hour
assert ret.minute == minute
$ pytest -v
....
test_main.py::test_get_now_with_timezone[('Asia/Tokyo', 10, 23, 30)-()-None] PASSED [ 33%]
test_main.py::test_get_now_with_timezone[('America/Los_Angeles', 10, 7...-()-None] PASSED [ 66%]
test_main.py::test_get_now_with_timezone[('UTC', 10, 14, 30)-()-None] PASSED [100%]
freeze_time
を使う際にtz_offsetを指定することでタイムゾーンを指定した状態の固定時間とすることができます。
時間の経過をシミュレートする
FreezeGunでは、固定した時刻から時間を進めることもできます。
from datetime import timedelta
def test_time_progression():
with freeze_time("2025-06-20 12:00:00") as frozen_time:
start_time = get_now_dt()
# 1時間進める
frozen_time.tick(delta=timedelta(hours=1))
ret = get_now_dt()
assert start_time.hour == 12
assert ret.hour == 13
assert (ret - start_time).total_seconds() == 3600
# 1時間30分進める
frozen_time.tick(delta=timedelta(hours=1, minutes=30))
ret2 = get_now_dt()
assert ret2.hour == 14
assert (ret2 - ret).total_seconds() == 5400
$ pytest -v
...
test_main.py::test_time_progression PASSED [100%]
tick()
メソッドを使用することで、固定された時刻から指定した時間だけ進めることができます。
まとめ
FreezeGunを使用することで、時間に依存するPythonコードのテストを安定して実行できるようになりました。デコレータやコンテキストマネージャーとして使用でき、時間の経過をシミュレートすることも可能です。日時に関する処理のテストを書く際には、積極的に活用していきたいツールです。
最後まで読んで頂いてありがとうございました。