pytest + FreezeGunで日時モックを使ったテストコードを書く

pytest + FreezeGunで日時モックを使ったテストコードを書く

Clock Icon2025.06.30

はじめに

データアナリティクス事業本部の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

テスト対象の関数

今回テストする関数は現在の日時を取得して文字列で返すシンプルな関数です。

main.py
from datetime import datetime

def get_now_dt():
    now = datetime.now()
    return now.strftime('%Y-%m-%d')

この関数は実行時の現在時刻に依存するため、通常のテストでは実行日によって結果が変わってしまいます。

FreezeGunを使ったテスト

FreezeGunを使用することで、テスト実行時の日時を固定できます。

test_main.py
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を使用することもできます。

test_main.py
@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

デコレータとコンテキストマネージャーを組み合わせた方法

デコレータとコンテキストマネージャーを組み合わせてみます。

test_main.py
@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を組み合わせることで、様々な日時パターンを効率的にテストできます。

時刻を含む詳細なテスト

日付だけでなく時刻も含めた詳細なテストも可能です。

test_main.py
@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はタイムゾーンも考慮したテストが可能です。

main.py
from datetime import datetime
import pytz

def get_now_with_timezone(timezone):
    tz = pytz.timezone(timezone)
    now = datetime.now(tz)
    return now
test_main.py
@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では、固定した時刻から時間を進めることもできます。

test_main.py
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コードのテストを安定して実行できるようになりました。デコレータやコンテキストマネージャーとして使用でき、時間の経過をシミュレートすることも可能です。日時に関する処理のテストを書く際には、積極的に活用していきたいツールです。

最後まで読んで頂いてありがとうございました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.