factory_boy
POSTの次は、GETリクエストを受け取ったときの挙動を確認するテストを実装していきましょう。これまで通りFakerを使うと、以下のように書けます。
def test_get(self):
fake = Faker()
data = {
'assignee': None,
'name': fake.word(),
'description': fake.text(),
'status': random.choice([x.value for x in TicketStatus]),
'start': str(fake.date_object()),
'end': None,
}
Ticket.objects.create(**data)
url = reverse('ticket-detail', kwargs={'pk': 1})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {
'id': 1,
'assignee': None,
'status_display': TicketStatus(data['status']).name,
'name': data['name'],
'description': data['description'],
'status': data['status'],
'start': data['start'],
'end': None,
})
これで問題なさそうですが、テストのためにTicket.objects.create(**data)という処理を用意するのは本質的ではなく冗長に感じます。またassigneeにも値をセットする場合は、ForeignKey先のUserモデルも作成する必要があり、またコードが冗長になってしまいます。この処理を簡略化するために、factory_boyというサードパーティ製のパッケージを使ってみましょう。
factory_boyは、Rubyのfactory_bot(旧 factory_girl)にインスパイアされたもので、フィクスチャを用意するためのFactoryクラスを作るためのパッケージです。Django以外にも、SQLAlchemyなど各O/Rマッパに対応しています。
それでは、factory_boyをインストールします。
(env) $ pip install factory_boy (env) $ vi requirements/test.txt (env) $ cat requirements/test.txt -r base.txt factory-boy==2.11.1 faker==0.8.16
以下のように、Metaクラスに対象となるモデルを指定してFactoryクラスを作成します。username、emailの各属性には、どのようなデータを生成するかが定義されています。factory.Faker()は先ほど紹介したFakerの処理を通してランダムな値を設定でき、factory.Sequence()は連番を利用してユニークな値を設定できます。
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
username = factory.Faker('name')
email = factory.Sequence(lambda n: f'person{n}@example.com')
>>> user = UserFactory() >>> user.username 'Kelly Jordan' >>> user.email 'person0@example.com' >>> UserFactory().email 'person1@example.com'
Ticketモデル用のFactoryクラスを作成するために、factories.pyファイルを準備します。kanban/board/直下に配置してもよいですが、テスト関連のファイルはテスト用のパッケージを新しく作ってそこにまとめることをお勧めします。
kanban/board/tests/パッケージとkanban/board/tests/factories.pyを作成してから、さきほどのkanban/board/tests.pyをkanban/board/tests/test_views.pyに変更しておきましょう。
ここまでで、ディレクトリ構成が以下のようになっていることを確認してください。
kanban
├── __init__.py
└── board
├── __init__.py
├── admin.py
├── apps.py
├── constants.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── serializers.py
├── tests # 新たにテスト用のパッケージを作成
│ ├── __init__.py
│ ├── factories.py # Factoryクラスのためのファイルを作成
│ └── test_views.py # kanban/board/tests.py から変更
└── views.py
kanban/board/tests/factories.pyを以下のように編集して、TicketFactoryを実装しましょう。
import factory.fuzzy
from ..constants import TicketStatus
from ..models import Ticket
class TicketFactory(factory.django.DjangoModelFactory):
class Meta:
model = Ticket
name = factory.Faker('word')
description = factory.Faker('text')
status = factory.fuzzy.FuzzyChoice([x.value for x in TicketStatus])
start = factory.Faker('date')
TicketFactoryがTicketインスタンスを生成するたびに、そのインスタンスの属性値は異なる結果となります。
>>> from kanban.board.tests.factories import TicketFactory >>> ticket = TicketFactory() >>> ticket.name 'home' >>> ticket.description 'Sing maybe house drive. Hard nice area center social. Amount environmental PM both any.\nPhone consider news different north. Concern term particularly.' >>> ticket.status 2 >>> ticket.assignee >>> >>> ticket.start '1986-06-09' >>> ticket.end >>> ticket = TicketFactory() >>> ticket.name 'material' >>>
assigneeとendは、TicketFactoryクラスの属性として実装されていないため、Ticketモデルで定義されているデフォルト値Noneがセットされています。せっかくなので、assigneeにも値が入るようにTicketFactoryクラスを編集しましょう。
assigneeに値をセットするためには、ForeignKey先のUserモデルを作成する必要があります。factory_boyでForeignKeyを扱うには、factory.SubFactory()を使います。SubFactoryは、依存関係にある各Factoryを再帰的に解決しながらインスタンスを組み立てます。
assignee = factory.SubFactory(UserFactory)と設定するために、まずはUserFactoryを実装しましょう。
import factory.fuzzy
from django.contrib.auth import get_user_model
from ..constants import TicketStatus
from ..models import Ticket
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = get_user_model()
django_get_or_create = ('username',)
username = factory.Faker('name')
email = factory.Sequence(lambda n: f'person{n}@example.com')
先ほど例として示したUserFactoryとほとんど同じですが、django_get_or_create = ('username',)というコードが新しく加わっています。usernameにはユニーク制約があるので、もし同じusernameのUserが作成された場合にエラーとなってしまいます。django_get_or_createにユニーク制約があるフィールドを設定しておくと、内部でModel.objects.create()の代わりにModel.objects.get_or_create()が実行されてエラーを回避できます。
UserFactoryの実装を終えたら、TicketFactoryにassignee = factory.SubFactory(UserFactory)を追加しましょう。
name = factory.Faker('word')
description = factory.Faker('text')
status = factory.fuzzy.FuzzyChoice([x.value for x in TicketStatus])
assignee = factory.SubFactory(UserFactory)
start = factory.Faker('date')
>>> ticket = TicketFactory() >>> ticket.assignee <User: Jeffrey Salinas> >>> ticket.assignee.email 'person0@example.com'
これでassigneeにも値を持たせられるようになりました。このTicketFactoryを使って、GETのテストを書き換えます。
def test_get(self):
ticket = TicketFactory()
url = reverse('ticket-detail', kwargs={'pk': ticket.id})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {
'id': ticket.id,
'assignee': ticket.assignee.username,
'status_display': ticket.get_status_display(),
'name': ticket.name,
'description': ticket.description,
'status': ticket.status,
'start': ticket.start,
'end': ticket.end,
})
Ticketインスタンスの生成処理が、TicketFactoryを使うことで1行になりすっきりしました。
これまで見てきたように、Fakerやfactory_boyを使うことでより良いテストを書くことができます。ぜひ皆さんも現場で活用してみてください。
