7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Django+Reactで学ぶプログラミング基礎(12): Djangoチュートリアル(投票アプリその5-3)

Last updated at Posted at 2022-06-08
[前回] Django+Reactで学ぶプログラミング基礎(11): Djangoチュートリアル(投票アプリその5-2)

はじめに

Django公式チュートリアル、その5-3です。
前回は、自動テストの重要性やテスト駆動開発を理解しました。
今回は、Djangoテストクライアントを使用し、ビューをテストします。

Djangoアプリ作成(その5-3): 投票(poll)アプリ

今回の内容

  • Djangoテストクライアント
  • ビューをテスト

クライアントツール: Djangoテストクライアント

  • Djangoテストクライアントとは
    • Djangoが提供するクライアントツール
    • ビューレベルで、ユーザとのインタラクションをシミュレート
      • ユーザが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
  • 質問を簡単作成するためのショートカット関数と、新しいテストクラスを作成
    image.png
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つのテストケースを一つずつ、手順に従って。。。
ぞっとして、考えたくもありません。

次回も続きます。お楽しみに。

[次回] Django+Reactで学ぶプログラミング基礎(13): Djangoチュートリアル(投票アプリその5-4)
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?