pytest で datetime.now() をモックする方法(freezegun, monkeypatch)

pytest で datetime.now() をモックする方法(freezegun, monkeypatch)

pytest で datetime.now() をモック化したい

pytest を使って datetime.now() で現在日時を取得しているコードをモック化してテストする方法をまとめます。

datetime.now() は実行時の現在日時を返すのでモック化しないとテストが難しい場合があります。ということでモック化したいですが datetime.now() は組み込み関数(属性)のため単純にモック化できません。例えば pytest-mock を使ってモック化すると、以下のようなエラーになります。

import datetime


def test_datetime_now(mocker):
    # datetime.now() をモック化して固定値を返す
    mocker.patch('datetime.datetime.now', return_value=datetime.datetime(2000, 1, 2, 3, 4))

    # 現在日時を取得
    now = datetime.now()

    # 検証
    assert now == datetime.datetime(2000, 1, 2, 3, 4)

    # FAILED test_sample.py::test_datetime_now - TypeError: can't set attributes of built-in/extension type 'datetime.datetime'

組み込みの関数をモック化するのは許されないみたいです。

ということで別の方法を考えます。freezegun というプラグインを使って datetime モジュールをモック化するのが簡単で広く使われているようです。

pytest-freezegun

pytest-freezegun · PyPI

$ pip install pytest-freezegun

pytest-freezegundatetime モジュールをモック化する pytest プラグインです。

pytest-freezegun をインストールすると、freezer というフィクスチャや freeze_time というマーカーが提供されます。

これらを使えば日時を固定したり変更したりできます。

@pytest.mark.freeze_time: テストメソッド内の datetime.now を固定する

マーカーを使ってテストメソッド内での日時を固定するコードです。テストメソッドが終了すれば、日時のモックは解除されます。

import datetime
import pytest

# datetime.now() を指定日時に固定する
@pytest.mark.freeze_time(datetime.datetime(2000, 1, 2, 3, 4))
def test_datetime_now():
    # 現在日時を取得
    now = datetime.datetime.now()

    # 検証
    assert now == datetime.datetime(2000, 1, 2, 3, 4)

freezer フィクスチャで datetime.now をモックしたり操作する

freezer フィクスチャを使えば日時をテストメソッド内で固定したり、変更したりできます。

freezer.move_to() で現在日時を指定の日時に固定できます。

import datetime
import pytest

def test_datetime(freezer):
    # 日時を変更する
    freezer.move_to(datetime.datetime(2000, 1, 2, 3, 4))
    now = datetime.datetime.now()
    assert now == datetime.datetime(2000, 1, 2, 3, 4)

    # 3分後にする
    freezer.move_to(datetime.datetime(2001, 1, 2, 3, 4) + datetime.timedelta(minutes=3))
    now = datetime.datetime.now()
    assert now == datetime.datetime(2001, 1, 2, 3, 4 + 3)

move_to を複数回テスト中に実行すれば、好きなタイミングで好きな時刻に変更できます。

便利ですね。

MagicMock を使って datetime.now だけをモックする

pytest でも datetime.datetime.now() を mock したい! – Qiita

上記ページを参考にしてます。

MagicMock(wraps=datetime.datetime)datetime モジュ。ルをラップしたモックオブジェクトを作成し、now() の処理だけモック化する方法です。モックオブジェクトを作成したら、monkeypatch で差し替えます。

import datetime
from unittest.mock import MagicMock


def test_datetime(monkeypatch):
    # MagicMock を作成し、datetime.now() だけをモック化する
    datetime_mock = MagicMock(wraps=datetime.datetime)
    datetime_mock.now.return_value = datetime.datetime(2000, 1, 2, 3, 4)
    # datetime モジュールをモックに差し替える
    monkeypatch.setattr(datetime, "datetime", datetime_mock)

    # 検証
    now = datetime.datetime.now()
    assert now == datetime.datetime(2000, 1, 2, 3, 4)

なにがしかの理由で freezegun やその他プラグインが使えないときにはこの方法が便利そうです。

参考URL

Pythonカテゴリの最新記事