SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

モダンDjango入門

Djangoのテストの書き方とCircleCIを活用した継続的インテグレーション

モダンDjango入門 第3回

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.pykanban/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')

 TicketFactoryTicketインスタンスを生成するたびに、そのインスタンスの属性値は異なる結果となります。

>>> 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を使うことでより良いテストを書くことができます。ぜひ皆さんも現場で活用してみてください。

次のページ
CircleCI

この記事は参考になりましたか?

モダンDjango入門連載記事一覧

もっと読む

この記事の著者

新井 正貴(アライ マサタカ)

 東京大学文学部卒業、アライドアーキテクツ株式会社にて勤務。2016年4月、株式会社SQUEEZEに入社。コミュニティ活動として、PyCon JP 2015〜 スタッフ、Pythonもくもく会の主催を行う。趣味はラクロスとPerfume。 Site:http://massa142.github.io/ Twitter:@massa142 Facebook:新井 正貴

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/10995 2018/08/03 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー