[Django REST Framework] pytest-freezegun🥶を使用して意図的に時間を経過させよう

## 概要
pytest-freezegunの使い方を紹介する
## 目的
pytest-freezegunの基本的な使い方を知ることができる

## 使用技術

```
python = 3.11.2
Django = 4.2.3
djangorestframework = 3.14.0
pytest = 7.3.1
factory-boy = 3.2.1
pytest-freezegun = 0.4.2
```

### テストの概要・APIの仕様について
今回はログアウトAPIを使用します
ログインすると1時間のセッションが保持されるはずですが、
本当に1時間でセッションの有効期限が切れるのかをテストします
セッションが切れていればログアウトAPIを叩いた時認証エラーとなります

※ログインには`client.login()`（返り値がbool）を使用しているので判定には`user.is_authenticated`を使用せず、ログアウトAPIを使用しています

## 早速みてみましょう

まずはライブラリをインストールしましょう
```
$ pip install pytest-freezegun
```

使用するフィクスチャを定義します
```conftest.py
import pytest

from datetime import datetime, timedelta
from django.core.management import call_command
from django.contrib.auth.models import Group
from rest_framework.test import APIClient

from app.models import Employee, User
from app.tests.factories.employee import EmployeeFactory
from app.tests.factories.user import UserFactory


@pytest.fixture(scope="session")
def django_db_setup(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():
        call_command("loaddata", "test.json")


@pytest.fixture
def admin_user():
    """admin権限ユーザーを作成"""
    group = Group.objects.get(name="admin")
    user = UserFactory(groups_id=group.id)
    return EmployeeFactory(user=user)


@pytest.fixture
def client(db):
    """APIクライアントを作成"""
    return APIClient()


@pytest.fixture
def login_user(client):
    """権限を指定してログインを実施"""

    def _method(employee):
        client.login(
        id=employee.id,
        password="password",
    )
    return _method


@pytest.fixture
def get_logout_url():
    """ログアウトAPIのURL"""
    return "/api/logout/"


@pytest.fixture
def freeze_time(freezer):
    """指定した時間まで経過させる"""

    def _method(hours, minutes, seconds):
        mock_now = datetime.now() + timedelta(
        hours=int(hours), minutes=int(minutes), seconds=int(seconds)
    )
        freezer.move_to(mock_now)

    return _method

@pytest.fixture
def unauthorized_message():
    """認証情報が含まれていない場合のエラーメッセージ"""
    return {"detail": "認証情報が含まれていません。"}


@pytest.fixture
def logout_message():
    """ログアウトが成功した場合のメッセージ"""
    return {"detail": "ログアウトしました"}


```

実際のテストコードです
59分59秒経過後はまだセッションが保持されているので、エラーとなりません

```test_logout.py
import pytest

from rest_framework import status


@pytest.mark.django_db
def test_admin_user_does_not_auto_logout_near_expiry(
    client, login_user, admin_user, get_logout_url, freeze_time, logout_message
):
    """admin権限ユーザーがログインして59分59秒経過後、自動的にログアウトされないか"""
    login_user(admin_user)
    freeze_time(0, 59, 59)
    response = client.post(get_logout_url, format="json")
    assert response.status_code == status.HTTP_200_OK
    assert response.json() == logout_message

```

しかし、1時間経過後にログアウトAPIを実行すると認証エラーとなります（セッションが切れている）
```logout.py
@pytest.mark.django_db
def test_admin_user_logout_session_expired(
    client, login_user, admin_user, get_logout_url, freeze_time, unauthorized_message
):
    """admin権限ユーザーがログインして1時間経過後、自動的にログアウトされるか"""
    login_user(admin_user)
    freeze_time(1, 0, 0)
    response = client.post(get_logout_url, format="json")
    assert response.status_code == status.HTTP_401_UNAUTHORIZED
    assert response.json() == unauthorized_message
```

## freezegunを使用したフィクスチャについて

```conftest.py
@pytest.fixture
def freeze_time(freezer):
    """指定した時間まで経過させる"""

    def _method(hours, minutes, seconds):
        mock_now = datetime.now() + timedelta(
        hours=int(hours), minutes=int(minutes), seconds=int(seconds)
    )
        freezer.move_to(mock_now)

    return _method
```

`freezer`はフィクスチャとしてすでに暗黙的に用意されています
`def freeze_time`の中に`def _method`として定義することでフィクスチャを利用する際に引数を渡すことができます

#### 使用例
```
freeze_time(0, 59, 59)
# 0時間59分59秒
freeze_time(1, 0, 0)
# 1時間0分0秒 
```

## 参考
- [pytest-freezegun PYPI](https://pypi.org/project/pytest-freezegun/)