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"
上記では
TimeStampedModel
を Abstract 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
Reference
この問題について(Django での created/updated/auto_now フィールドのテスト), 我々は、より多くの情報をここで見つけました
https://dev.to/serhatteker/testing-createdupdatedautonow-fields-in-django-139k
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
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()
# 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
日時のインポート
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
Reference
この問題について(Django での created/updated/auto_now フィールドのテスト), 我々は、より多くの情報をここで見つけました https://dev.to/serhatteker/testing-createdupdatedautonow-fields-in-django-139kテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol