Django での created/updated/auto_now フィールドのテスト


はじめに



Django の auto_now_add および auto_now モデル フィールド引数を使用すると、エンティティの作成時および/または最終更新時に日付を簡単に作成できます.

より正確な概要を説明するために、以下のような投稿 Model があると仮定します.

# src/main/models.py
from django.utils.translation import ugettext_lazy as _

from src.core.models.abstract import TimeStampedModel


class Post(TimeStampedModel):
    title = models.CharField()
    author = models.ForeignKey("author")
    body = models.TextField()
    is_published = models.BooleanField(default=False)

    class Meta:
        verbose_name = _("Post")
        verbose_name_plural = _("Posts")

    def __repr__(self):
        return f"<Post {self.author}:{self.title}>"

    def __str__(self):
        return f"{self.author}:{self.title}"


そして、これは私の TimeStampedModel です:

# src/core/models/abstract.py
from django.db import models


class TimeStampedModel(models.Model):
    """
    An abstract base class model that provides self-updating
    `created_at` and `updated_at` fields.
    """

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
        ordering = ["-updated_at"]
        get_latest_by = "-updated_at"


上記では TimeStampedModelAbstract Model として使用しましたが、共通フィールドを Abstract Model に移動し、クリーンなアーキテクチャと DRY メソッドに合わせることを常にお勧めします.

より一般的な方法では、以下のようにモデルを記述できます.

```python {linenos=table}

src/main/models.py



django.dbインポートモデルから
django.utils.translationから、ugettext_lazyを_としてインポートします

クラス Post(models.Model):
タイトル = models.CharField()
著者 = models.ForeignKey("著者")
本体 = モデル.TextField()
is_published = models.BooleanField (デフォルト = False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
    verbose_name = _("Post")
    verbose_name_plural = _("Posts")

def __repr__(self):
    return f"<Post {self.author}:{self.title}>"

def __str__(self):
    return f"{self.author}:{self.title}"





## Problem

Unfortunately, `auto_now` and `auto_now_add` make writing unit tests which depend on creation or modification times difficult, since there is no simple way to set these fields to a specific time for testing.

For an example, assume you have a business rule; you're giving 7 days to authors to be able publish a blog post after creation. Maybe you want them to re-read their posts to eliminate typo or logic errors — yes, it makes no sense, I'm just making some company policy.



```python
# src/main/models.py
import datetime

from django.utils.translation import ugettext_lazy as _
from django.utils import timezone

from src.core.models.abstract import TimeStampedModel


class Post(TimeStampedModel):
    title = models.CharField()
    author = models.ForeignKey("author")
    body = models.TextField()
    is_published = models.BooleanField(default=False)

    class Meta:

        verbose_name = _("Post")
        verbose_name_plural = _("Posts")

    def __repr__(self):
        return f"<Post {self.author}:{self.title}>"

    def __str__(self):
        return f"{self.author}:{self.title}"

    def publish(self):
    """Publish a post which created >=7 days before"""
        if timezone.now() - self.created_at <= datetime.timedelta(days=7):
            self.is_published = True
            self.save()

ご覧のとおり、投稿が 7 日以上前に作成された場合、is_published attr True を作成しています.

この動作をテストするために、単体テストを書きましょう.

# src/tests/test_models.py
import pytest

from src.main.models import Post
from src.users.models import User
from src.users.tests.factories import UserFactory


@pytest.fixture
def user() -> User:
    return UserFactory()


class TestPostModel:
    def test_is_published_with_now(self, user):
        post = Post.objects.create(
            title="some-title",
            body="some-body",
            author=user,
        )

        post.publish()
        assert post.is_published is True

FactoryBoy ライブラリを使用して User インスタンスを作成していますが、 User.objects.create(...) などのデフォルトのメソッドを使用できます.

作成された created_at モデル インスタンスの Post フィールドは常にテストを実行した時間と同じになるため、上記のテストは失敗します.したがって、テストで is_published True を作成する方法はありません.

解決



解決策は Python の unittest.mock ライブラリから来ています: [Mock];

```python {linenos=table, hl_lines=[4,21]}

src/tests/test_models.py



日時のインポート
pytest をインポート
ユニットテストインポートモックから

django.utilsインポートタイムゾーンから

from src.main.models import Post
src.users.modelsインポートユーザーから
src.users.tests.factories から UserFactory をインポート

@pytest.fixture
def user() -> ユーザー:
UserFactory() を返す

クラス TestPostModel:
time_test_now = timezone.now() - datetime.timedelta(日=60)

@mock.patch("django.utils.timezone.now")
def test_is_published_with_now(self, mock_now, user):
    mock_now.return_value = self.time_test_now
    post = Post.objects.create(
        title="some-title",
        body="some-body",
        author=user,
    )

    post.publish()
    assert post.is_published is True



We are patching the method with `mock.patch` decorator to return a specific time when the factory creates the object for testing. So with `mock` in this method *current* now will be 60 days before *actual* now.

When you run the test you'll see the test will pass.

Instead of decorator you can also use `context manager` —I don't usually use this method since it creates hard-to-read, nested methods when you mock/patch multiple stuff:




```python {linenos=table, hl_lines=[19]}
# src/tests/test_models.py
import pytest
from unittest import mock

from django.utils import dateparse, timezone

from src.main.models import Post
from src.users.models import User
from src.users.tests.factories import UserFactory


@pytest.fixture
def user() -> User:
    return UserFactory()


class TestPostModel:
    def test_is_published_with_now(self, user):
        with mock.patch("django.utils.timezone.now") as mock_now:
            mock_now.return_value = dateparse.parse_datetime("2020-01-01T04:30:00Z")
            post = Post.objects.create(
                title="some-title",
                body="some-body",
                author=user,
            )

        post.publish()
        assert post.is_published is True


テストを実行すると、同じ成功した結果が表示されます.

すべて完了!

[モック]: https://docs.python.org/3/library/unittest.mock.html
[Django - 抽象基本クラス]: https://docs.djangoproject.com/en/4.0/topics/db/models/#abstract-base-classes