[前回] Django+Reactで学ぶプログラミング基礎(11): Djangoチュートリアル(投票アプリその5-2)
はじめに
Django公式チュートリアル、その5-3です。
前回は、自動テストの重要性やテスト駆動開発を理解しました。
今回は、Djangoテストクライアントを使用し、ビューをテストします。
Djangoアプリ作成(その5-3): 投票(poll)アプリ
今回の内容
- Djangoテストクライアント
- ビューをテスト
クライアントツール: Djangoテストクライアント
- Djangoテストクライアントとは
- Djangoが提供するクライアントツール
- ビューレベルで、ユーザとのインタラクションをシミュレート
- ユーザがWebブラウザを通して経験する動作をチェック
- コード内部の細かい動作には焦点を当てない
- ユーザがWebブラウザを通して経験する動作をチェック
-
tests.py
コードやshell
コマンドで使用可能
shellからテストクライアントを使用
- テスト環境をセットアップ
C:\kanban\pollsite>..\venv\.venv\Scripts\activate
(venv) C:\kanban\pollsite>python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
- テストクライアントのクラスをインポート
- ※ テストクライアントを
tests.py
コードで使用する場合はインポート不要-
django.test.TestCase
クラス自体がクライアントを持っているため
-
- ※ テストクライアントを
>>> from django.test import Client
- テストクライアントのインスタンスを作成
>>> client = Client()
- クライアントツールで、URL
'/'
へリクエスト発行し、レスポンス取得
>>> response = client.get('/')
Not Found: /
- レスポンスのステータスコードを確認(404は、
Not Found
)
>>> response.status_code
404
- URL
'/polls/'
へリクエスト-
'reverse()'
を使用し、URL取得
-
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/7/">\xe4\xbb\x8a\xe6\x97\xa5\xe3\x81\xae\xe4\xba\x88\xe5\xae\x9a\xe3\x81\xaf?</a></li>\n \n <li><a href="/polls/6/">\xe6\xa5\xbd\xe3\x81\x97\xe3\x81\x84\xe3\x81\x93\xe3\x81\xa8\xe3\x81\xaf\xef\xbc\x9f</a></li>\n \n <li><a href="/polls/5/">\xe5\xb9\xb8\xe3\x81\x9b\xe3\x81\xa8\xe3\x81\xaf\xef\xbc\x9f</a></li>\n \n <li><a href="/polls/4/">\xe7\x9b\xae\xe6\xa8\x99\xe3\x81\xaf\xef\xbc\x9f</a></li>\n \n <li><a href="/polls/3/">\xe8\xb6\xa3\xe5\x91\xb3\xe3\x81\xaf\xef\xbc\x9f</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: 今日の予定は?>, <Question: 楽しいことは?>, <Question: 幸せとは?>, <Question: 目標は?>, <Question: 趣味は?>]>
>>> exit()
ビューを改良
- 投票アプリの問題点
- 未公開の投票(
pub_date
フィールドが未来)も、表示されてしまう
- 未公開の投票(
-
Question
ビューを改善-
pub_date
の日付になった時に公開する、それまでは表示しない
-
ListView
クラスをベースとするIndexView
ビューを修正
- インポート文を追加
polls/views.py
from django.utils import timezone
-
get_queryset()
メソッドを修正-
pub_date
日付をtimezone.now()
と比較、-
timezone.now
以前のQuestion
を含んだクエリセットを返す
-
-
polls/views.py
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
新しいビューをテスト
- 期待通り満足のいく動作をしてくれるか、手動で確かめる手順
- まず、
runserver
でサーバー起動 - ブラウザでサイトを読み込む
- 過去と未来、それぞれの日付を持つ質問を作成
- すでに公開されている質問だけがリストに表示されるか確認
- まず、
- プロジェクトにわずかな変更を加えるたびに、上記手順を手動で確認するのはしんどい
- 対処: 自動テストを作成(上記、
shell
で実行した内容をベースに)
- 対処: 自動テストを作成(上記、
テストを作成
- まず、
polls/tests.py
に次の行を追加
from django.urls import reverse
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question2, question1],
)
コード説明
-
create_question
- 質問を作成するためのショートカット関数
- 質問作成処理のコード重複をなくしている
- 質問を作成するためのショートカット関数
-
test_index_view_with_no_questions
- 新規質問は作成しない
-
No polls are available.
というメッセージが表示されるかチェック -
latest_question_list
が空になっているか確認 -
django.test.TestCase
クラスが提供するアサーションメソッドを使用assertContains()
assertQuerysetEqual()
-
test_index_view_with_a_past_question
- 質問を作成し、その質問がリストに現れるか検証
-
test_index_view_with_a_future_question
-
pub_date
が未来となる質問を作成- データベースは各テストメソッドごとにリセットされる
- 索引ページに、過去作成した質問は残っていない
- データベースは各テストメソッドごとにリセットされる
-
作成したテストを実行
(venv) C:\kanban\pollsite>python manage.py test polls
Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.069s
OK
Destroying test database for alias 'default'...
8つのテストケースを一瞬で通し、結果OKです。すごい。
おわりに
自動テストの作成に手間はかかるけど、
作ってしまえば、アプリを修正する度に再利用できます。
作業効率がぐんと上がりますね。
自動テストでなく、画面上で手動による動作確認はどうでしょうか。
8つのテストケースを一つずつ、手順に従って。。。
ぞっとして、考えたくもありません。
次回も続きます。お楽しみに。