【Django】はじめてのDjangoアプリ第11回

django

PythonのWEBフレームワークDjangoを利用したアプリケーションの公式テュートリアルに沿ったWEBアプリケーション作成のメモ第11回です。

Writing your first Django app, part 5 | Django documentation
The web framework for perfectionists with deadlines.

前回はロジックの自動テストを実施しました。

今回はビューの自動テストを実施していきます。

polls アプリケーションはかなり無差別です。pub_date フィールドが未来のものを含め、あらゆる質問を公開します。これを改善すべきです。 pub_date を未来に設定すると、質問はその時点で公開されますが、その日付までは非表示になります。

上記のバグを修正したとき、最初にテストを作成し、次にそれを修正するコードを作成しました。実際、これはテスト駆動開発の一例でしたが、作業をどの順序で行うかは実際には重要ではありません。

最初のテストでは、コードの内部動作に重点を置きました。このテストでは、ユーザーが Web ブラウザーを通じて体験する動作を確認したいと考えています。

何かを修正する前に、利用できるツールを見てみましょう。

Django テストクライアント

Django は、ビュー レベルでコードを操作する、ユーザーをシミュレートするテスト クライアントを提供します。これは、tests.py または shell 内でさえ使用できます。

shell から再び始めます。そこでは、tests.py では必要のないいくつかのことを行う必要があります。 1 つ目は、shell でテスト環境をセットアップすることです。

python manage.py shell
from django.test.utils import setup_test_environment
setup_test_environment()

setup_test_environment() はテンプレート レンダラーをインストールします。これにより、response.context など、他の方法では利用できない応答の追加属性を調べることができます。この方法ではテスト データベースが設定されないため、次のコードは既存のデータベースに対して実行され、出力はすでに作成した質問に応じて若干異なる場合があることに注意してください。 settings.pyTIME_ZONE が正しくないと、予期しない結果が生じる可能性があります。以前に設定した覚えがない場合は、続行する前に確認してください。

次に、テスト クライアント クラスをインポートする必要があります (後ほど、tests.pydjango.test.TestCase クラスを使用します。これには独自のクライアントが付属しているため、これは必要ありません)。

from django.test import Client
# Clientのインスタンス生成
client = Client()

それが準備できたら、クライアントにいくつかの作業を依頼できます。

# '/' から応答を取得
response = client.get("/")
# 404が返る
# "Invalid HTTP_HOST header"や400エラーが返る場合は
# おそらく setup_test_environment() が省略されています
response.status_code
# 一方で、'/polls/'では何かしらの応答があります
# 直書き URL ではなく「reverse()」を使用します。
from django.urls import reverse
response = client.get(reverse("polls:index"))
response.status_code
response.content
response.context["latest_question_list"]

ビューの修正

投票のリストには、まだ公開されていない投票 (つまり、pub_date が未来の投票) が表示されます。それを修正しましょう。

チュートリアル 4 では、ListView に基づくクラスベースのビューを導入しました。

▼「polls/views.py」で前回記述した内容

class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]

get_queryset() メソッドを修正し、timezone.now() と比較して日付もチェックするように変更する必要があります。まずimport を追加する必要があります。

▼「polls/views.py」を編集

※冒頭に追記

from django.utils import timezone

次に、 get_queryset メソッドを次のように修正する必要があります。

    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
        ]

Question.objects.filter(pub_date__lte=timezone.now()) は、pub_datetimezone.now 以下、つまり timezone.now 以上である Question を含むクエリセットを返します。

新しいビューのテスト

これで、runserver を起動し、ブラウザにサイトをロードし、過去と未来の日付を含む Question を作成し、公開されたものだけがリストされていることを確認することで、これが期待どおりに動作することを確認できます。これに影響を与える可能性のある変更を加えるたびにこれを行う必要はありません。そのため、上記の shell セッションに基づいてテストも作成しましょう。

以下をpolls/tests.pyに追加します。

▼「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],
        )

これらのいくつかをさらに詳しく見てみましょう。

1 つ目は、質問のショートカット関数 create_question で、質問を作成するプロセスの繰り返しを省きます。

test_no_questions は質問を作成しませんが、「No polls are available.」 (利用可能な投票はありません) というメッセージをチェックします。そして、latest_question_list が空であることを確認します。 django.test.TestCase クラスは追加のアサーション メソッドをいくつか提供していることに注意してください。これらの例では、assertContains()assertQuerySetEqual() を使用します。

test_past_question では、質問を作成し、それがリストに表示されることを確認します。

test_future_question では、未来の pub_date を使用して質問を作成します。データベースはテスト方法ごとにリセットされるため、最初の質問は存在しなくなり、インデックスには質問が含まれなくなります。

等々。実際、私たちはテストを使用してサイト上の管理者の入力とユーザー エクスペリエンスを伝え、システムのすべての状態および新しい状態の変更ごとに、期待される結果が公開されることを確認しています。

DetailView のテスト

私たちが持っているものはうまく機能します。ただし、今後の質問がインデックスに表示されない場合でも、ユーザーは正しい URL を知っているか推測できれば、その質問にアクセスできます。したがって、同様の制約を DetailView に追加する必要があります。

▼「polls/views.py」を編集

※「DetailView()」の箇所に追記

    def get_queryset(self):
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())

次に、いくつかのテストを追加して、pub_date が過去の Question は表示できるか、pub_date が未来の Question は表示できないことを確認する必要があります。

▼「polls/tests.py」を編集

※末尾に追記

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text="Future question.", days=5)
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

▼実行結果

今回は以上です。次回は静的ファイルの管理を予定しています。

  • 0
  • 0
  • 0
  • 0

コメント

タイトルとURLをコピーしました