SHOEISHA iD

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

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

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

モダンDjango入門

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

モダンDjango入門 第3回

Faker

 POSTの正常系のテストができるようになりましたが、これではまだ1パターンのデータでしかテストができていません。もしかしたら偶然うまくいっているだけで、違うデータをPOSTしたら失敗する可能性も考えられます。固定値を使うのではなく、より現実に近いランダムな値で検査してみましょう。

 ここではFakerというサードパーティのパッケージを使って、テストデータの生成を行います。

 それではまず、Fakerをインストールしてみましょう。

(env) $ pip install faker
(env) $ vi requirements/test.txt
(env) $ cat requirements/test.txt
-r base.txt
faker==0.8.16

 requirements/test.txtというファイルを新しく作成して、テスト用のパッケージをここで管理するようにします。

 faker.Faker()の引数にlocaleを渡して初期化して、各用途に応じたメソッドを呼び出せばOKです。

>>> from faker import Faker
>>> fake = Faker()
>>> fake.name()
'Rebecca Lopez'
>>> fake.name()
'Penny Obrien'
>>> fake.address()
'9645 Kim Parkways\nLake Scott, UT 34764'
>>> fake = Faker('ja_JP')
>>> fake.name()
'笹田 健一'
>>> fake.name()
'山本 太郎'
>>> fake.address()
'佐賀県四街道市台場7丁目15番10号 池之端アーバン120'

 Fakerを使って、さきほどのTicketTestsをランダムなデータによるテストに書き換えます。data変数を以下のように編集しましょう。またstatusの値に関しては、IntEnumであるTicketStatusとrandom.choice()を組み合わせて1〜3の範囲内で値を取得するようにしています。

import json
import random

from django.test import TestCase
from django.urls import reverse
from faker import Faker
from rest_framework import status

from .constants import TicketStatus
from .models import Ticket


class TicketTests(TestCase):
    def test_post(self):
        fake = Faker()
        url = reverse('ticket-list')
        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,
        }
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Ticket.objects.count(), 1)
        self.assertEqual(Ticket.objects.get().name, 'test ticket')

 これを実行してみると、次のようなエラーが返ってきてテストが失敗してしまいます。

(env) $ python manage.py test kanban
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_post (board.tests.TicketTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/massa142/src/github.com/massa142/modern-django/kanban/board/tests.py", line 28, in test_post
    self.assertEqual(Ticket.objects.get().name, 'test ticket')
AssertionError: 'worry' != 'test ticket'
- worry
+ test ticket


----------------------------------------------------------------------
Ran 1 test in 0.037s

FAILED (failures=1)
Destroying test database for alias 'default'...

 この原因は、作成したチケットのnameの期待値をtest ticketから変更していないためです。28行目を次のように修正して、再びテストが成功することを確認してください。

        self.assertEqual(Ticket.objects.get().name, data['name'])

境界値分析

 POSTの正常系のテストができたので次は、TicketSerializerで実装した開始日と終了日に関するバリデーションをテストしましょう。

class TicketSerializer(serializers.ModelSerializer):
...

    def validate(self, data):
        start = data['start']
        end = data['end']

        if start and end and start > end:
            raise serializers.ValidationError('終了日は開始日以降に設定してください')

        return data

 このバリデーションがきちんと動いているかを確かめるために、妥当ではないdataをPOSTするテストを書きます。

    def test_validate_opposite_dates(self):
        fake = Faker()
        url = reverse('ticket-list')
        data = {
            'assignee': None,
            'name': fake.word(),
            'description': fake.text(),
            'status': random.choice([x.value for x in TicketStatus]),
            'start': str(fake.future_date()),
            'end': str(fake.past_date()),
        }
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(Ticket.objects.count(), 0)
        self.assertEqual(response.data, {'non_field_errors': ['終了日は開始日以降に設定してください']})

 fake.future_date()fake.past_date()を用いて、開始日に未来の日付を、終了日に過去の日付をセットしています。

 先ほどとは異なり、ここでは以下の3つのアサーションを行っています。

  • ステータスコードが400かどうか
  • 作成されたレコードがないかどうか
  • エラーメッセージが正しいかどうか

 これでバリデーションが期待した動きになっているかを確かめることができました。しかしこれではまだ安心できません。プログラムを書く上で、バグが発生しやすい箇所の1つが境界値です。バリデーションの中にstart > endというロジックが含まれているので、開始日と終了日が同じになる場合もテストしておきましょう。

    def test_validate_same_dates(self):
        fake = Faker()
        url = reverse('ticket-list')
        date = str(fake.date_object())
        data = {
            'assignee': None,
            'name': fake.word(),
            'description': fake.text(),
            'status': random.choice([x.value for x in TicketStatus]),
            'start': date,
            'end': date,
        }
        response = self.client.post(url, json.dumps(data), content_type='application/json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Ticket.objects.count(), 1)
        self.assertEqual(Ticket.objects.get().name, data['name'])

 これで開始日と終了日が同じ日付でも、チケットの登録が可能であることを担保できました。

次のページ
factory_boy

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

モダン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」など、さまざまなカンファレンスを企画・運営しています。

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

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

メールバックナンバー