プログラミング初心者の勉強ブログ #98
Pythonオンライン学習教材として評判のPyQ(https://pyq.jp/)によるPython学習記録です。Rubyを多少かじり、第二外国語としてPythonを少しづつ学んでいきます。(内容はRubyとの比較が多くなるかもしれません。)
PyQがやたらテストをさせてくるので今回はDjangoのdjango.test.TestCaseでのテストの書き方についてまとめます。RailsでもRspecがあったがあまりやらなかったのでDjangoでもう一度確認していきます。後半はPyQを2週間ほどやっての感想です。
目次
目次
Djangoでテストを書いていく
テストの実行
実行はターミナルで行う。
1 2 |
cd ec python manage.py test (実行させたいテスト) |
ecディレクトリに移動後、manage.pyを起動させ、「test」の後ろに実行したいテストファイルの関数を入力する。
テストファイルの作成
テストファイルは「views.py」と同じ階層に作成した「test」ディレクトリ内に入れる。そのため、対象のモジュール毎に1つのテストディレクトリが作られる。このとき、ファイル名の頭は「test〜」とし、test_views.py、test_models.py、test_forms.pyのようにテストファイルをモデルやビューそれぞれで書き込んでいく。
上の画像の場合、「tickets」直下にview.pyが存在し、同じくtestディレクトリが同階層に存在する。testディレクトリ内は「forms」や「models」「views」などtickets内のそれぞれのpyファイルのコード毎にテストファイルを作成するのが一般的。
テストコードの作成
先ほどの画像で示したファイル構成の中の、実際のtest_views.pyのコードを見ながら確認していく。
下のコードは、「チケット販売ECサイトのチケット一覧機能」をテストしている。チケットにはカテゴリーが存在し、categoryモデルが別に存在する。
tickets/test/test_views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// テストクラスに継承させるTestCaseをインポート from django.test import TestCase // reverse関数のインポート(URLの逆引きに必要) from django.urls import reverse // tickets直下のmodelsをインポート from tickets import models // tickets直下のtestingをインポート(factory_categoryとfactory_ticket関数が定義されている) from tickets import testing // Ticketというモデルのリスト作成に関するテストコードを定義するクラスを作成 class TestTicketList(TestCase): # URLを返す関数 def _getTarget(self): return reverse('tickets:list') def test_get(self): # factory_category関数(下に記載)でカテゴリーのテスト用データを生成 c1 = testing.factory_category(display_priority=10) c2 = testing.factory_category(display_priority=5) # factory_ticket関数(下に記載)でチケットのテスト用データを生成 t1 = testing.factory_ticket(category=c1, start_date='2016-11-05') t2 = testing.factory_ticket(category=c1, start_date='2016-11-08') t3 = testing.factory_ticket(category=c2) # リクエストを擬似的に送ってくれるHTTPクライアント(self.cliant)でレスポンスオブジェクトを生成 res = self.client.get(self._getTarget()) # assertTemplateUsed関数でレスポンスオブジェクトが任意のテンプレートを使用しているかか判定 self.assertTemplateUsed(res, 'tickets/list.html') # assertEqualでレスポンスオブジェクトの要素が予想と一致しているか判定 # res.contextはテンプレートに渡したdictが入っている(正しくticketsがソートされているかの確認) self.assertEqual(len(res.context['tickets']), 3) self.assertEqual(res.context['tickets'][0], t1) self.assertEqual(res.context['tickets'][1], t2) self.assertEqual(res.context['tickets'][2], t3) # 省略 |
tickets/testing.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// 同階層ディレクトリのmodelsをインポート from . import models // 別で存在するtbpauthディレクトリのtesting.pyからfactory_userをインポート from tbpauth.testing import factory_user def factory_category(**kwargs): """ テスト用のCategoryのデータを作る """ # それぞれのカラム(nameなど)の値を指定 d = { 'name': 'テストカテゴリー', 'extra_fee_rate': 0.0, 'display_priority': 0, } # 引数で指定した値でその他のカラムを更新(display_priorityカラム) d.update(kwargs) # dで指定した値でcategoryモデルのオブジェクトを生成し返却 return models.Category.objects.create(**d) def factory_ticket(**kwargs): """ テスト用のTicketのデータを作る """ # それぞれのカラム(nameなど)の値を指定 d = { 'name': 'テストチケット', 'start_date': '2016-11-05', 'price': 1000, 'quantity': 10, } # 引数で指定した値でその他のカラムを更新(category、start_dateなど) d.update(kwargs) # sellerカラムの分岐(入ってなかったらfactory_user(下に記載)でユーザーを生成) if 'seller' not in d: d['seller'] = factory_user() # categoryカラムの分岐(入ってなかったらカテゴリーを生成) if 'category' not in d: d['category'] = factory_category() # dで指定した値でticketモデルのオブジェクトを生成し返却 return models.Ticket.objects.create(**d) |
tbpauth/testing.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// random関数をインポート import random // 同階層のmodelsをインポート from . import models def factory_user(**kwargs): # それぞれのカラム(usernameなど)の値を指定 d = { 'username': ''.join(random.choice('abcdef') for _ in range(10)), 'first_name': '名', 'last_name': '姓', 'email': 'test@example.com', 'address1': '住所1', 'address2': '住所2', } # 指定した引数の中にpasswordがあればvalueを引数から抜き取る、なければNoneを代入 password = kwargs.pop('password', None) # 引数で指定した値でその他のカラムを更新 d.update(kwargs) # dで指定した値でticketモデルのオブジェクトを生成 user = models.User(**d) # passwordがあればuserモデル定義したset_password関数でパスワードを指定 if password: user.set_password(password) # ユーザーの保存 user.save() # 保存したユーザーを返却 return user |
ポイント
地味に書かれているが
1 2 3 |
# URLを返す関数 def _getTarget(self): return reverse('tickets:list') |
ここと、
1 2 |
# リクエストを擬似的に送ってくれるHTTPクライアント(self.cliant)でレスポンスオブジェクトを生成 res = self.client.get(self._getTarget()) |
ここをちゃんと理解しておきたい。この_getTarget関数は、テストクラス内で「self._getTarget()」の形で使われている。要は「return reverse('tickets:list')」が何をしているかだが、reverseはURLをパスの名前から逆引きしてくれる関数で、'reverse('tickets:list')'によって'tickets/list'というパスをreturnしている。
「self.client.get(self._getTarget())」によって、「GETで'tickets/list'へのリクエスト」をするHTTPクライアントに対するレスポンスを返り値として返す。
あとはassertTemplateUsedやassertEqual、レスポンスオブジェクトに対するcontextなどの各種メソッドの使い方を理解しておけば、とりあえず読むことはできる気がする。
その他
箇条書きしていく。
- テストで使用したデータはテスト終了後自動で削除されるため、後で消す必要はない。
- 今回のようなView関数のテストにあたっては、データがちゃんと表示されているかに加え、並び順がちゃんとソートされた通りになっているかも確認すべき。
- assertEqualなどのメソッドは他にも複数存在するため、ある程度把握しておくべきかもしれない。
- Python組み込みのunittestととは少し違うので注意する。
まとめ
PyQの難易度が高く感じます。基本的に前半部は写経中心の進み、最後にアウトプットチャンスとして自力で解かなければならない問題が出題されるのですが、今回やった「Django中級」では、写経でやった内容より何段階かレベルが上がった問題を最後に出してくるので、結局わからず模範解答の写経になります。写経学習を行うにあたって何が怖いかといえば、気づいたらタイピングゲームになってしまいかねない点です。もともとPyQの学習目的はPythonとDjangoのコードが読めるようになることなのであれですが、写経中心の学習だと一人でDjango使ってアプリ作るレベルまでまず到達できないと感じます。自分のローカルでPythonとDjangoの環境構築をしなくても学習できるのは便利だと思いますが、ローカルで順々に作成していきたいのか正直なところです。ソースコードがPyQサイト内にしか存在しないので、解約してしまうとコードが見れない訳です。ローカルで順々に開発できればソースコードが自分のPCに残る訳で。これは戦略的なのものなのかはわかりません。3000円のコースのくせに贅沢言うなと怒られそうです。こんな感想が出てしまうということは、何十万もするプログラミングスクールの手厚いフォローにゆとられて育ってしまったからなんだと思います。文句ばかり言ってしまってますが、Python学習教材としてPyQはおすすめの教材なんだと思います。実際Pythonの基礎学習はかなりいい感じに進めることができ、勉強がはかどりました。また、今回の「Django中級」は、何か一つのアプリを一から順々に作っていくスタイルではなく、ある程度作られた状態のコードから自分で機能を追加していくというスタイルのため、コードを読み解いていかなければなりません。「新人エンジニア時代は実際の業務でいちから開発をはじめることはほとんどなく、先輩エンジニアが用意した環境に機能を追加する仕事が多いです。」とPyQに書かれてたので、こちらのスタイルの方が確かに実践寄りで良いのかもしれません。後半になってクエストの問題が模範解答を見ずに解けなくなったことに対する悔しさがこのような文句を生んでいるだけです。なんか言ってるよくらいに聞き流して欲しいです。ただ、ローカル環境で一から順々にアプリを作成したかったなーと。僕の現状のPyQの感想はこんな感じです。
以上ありがとうございました。