最初のDjangoアプリの作成、パート5
このチュートリアルは、 Tutorial 4 が中断したところから始まります。 Web-pollアプリケーションを作成しました。次に、そのアプリケーションの自動テストをいくつか作成します。
自動テストの紹介
自動テストとは何ですか?
テストは、コードの動作をチェックするルーチンです。
テストはさまざまなレベルで動作します。 一部のテストは細部に適用される場合があり(特定のモデルメソッドは期待どおりに値を返しますか?)、他のテストはソフトウェアの全体的な操作を調べます(サイトでの一連のユーザー入力は望ましい結果?)。 これは、 Tutorial 2 で以前に行った、:djadmin: `shell` を使用してメソッドの動作を調べたり、アプリケーションを実行してデータを入力したりするテストと同じです。それがどのように動作するかを確認します。
自動テストの違いは、テスト作業がシステムによって行われることです。 一連のテストを一度作成すると、アプリに変更を加えるときに、時間のかかる手動テストを実行しなくても、コードが当初の意図どおりに機能することを確認できます。
テストを作成する必要がある理由
では、なぜテストを作成するのか、そしてなぜ今なのか?
Python / Djangoを学ぶだけで十分だと感じるかもしれませんし、さらに別のことを学び、実行することは、圧倒され、おそらく不必要に思えるかもしれません。 結局のところ、私たちの投票アプリケーションは現在非常にうまく機能しています。 自動化されたテストを作成するという問題を経験しても、それがうまく機能することはありません。 投票アプリケーションの作成がDjangoプログラミングの最後の部分である場合は、自動テストの作成方法を知る必要はありません。 しかし、そうでない場合は、今が学ぶのに最適な時期です。
テストはあなたの時間を節約します
ある時点までは、「動作しているように見えることを確認する」ことで十分なテストになります。 より洗練されたアプリケーションでは、コンポーネント間に数十の複雑な相互作用がある場合があります。
これらのコンポーネントのいずれかを変更すると、アプリケーションの動作に予期しない結果が生じる可能性があります。 それがまだ「機能しているように見える」ことを確認することは、テストデータの20の異なるバリエーションを使用してコードの機能を実行し、何かが壊れていないことを確認することを意味する可能性があります。
これは、自動テストで数秒でこれを実行できる場合に特に当てはまります。 何か問題が発生した場合、テストは予期しない動作を引き起こしているコードを特定するのにも役立ちます。
特にコードが適切に機能していることがわかっている場合は、生産的で創造的なプログラミング作業から離れて、魅力的で刺激のないテスト作成ビジネスに直面するのは面倒に思えるかもしれません。
ただし、テストを作成するタスクは、アプリケーションを手動でテストしたり、新しく発生した問題の原因を特定しようとしたりするよりもはるかに充実しています。
テストは問題を特定するだけでなく、問題を防ぎます
テストを単に開発のネガティブな側面と考えるのは間違いです。
テストがないと、アプリケーションの目的や意図された動作はかなり不透明になる可能性があります。 それがあなた自身のコードであるときでさえ、あなたは時々それが何をしているのかを正確に見つけようとしてそれを突っついていることに気付くでしょう。
テストはそれを変えます。 彼らはあなたのコードを内側から照らし、何かがうまくいかないとき、彼らは間違っている部分に光を当てます-あなたがそれが間違っていることに気づいていなくても。
テストはあなたのコードをより魅力的にします
あなたは素晴らしいソフトウェアを作成したかもしれませんが、他の多くの開発者はテストが不足しているためにそれを見ることを拒否するでしょう。 テストがなければ、彼らはそれを信用しません。 Djangoの最初の開発者の1人であるJacobKaplan-Mossは、「テストのないコードは設計によって壊れています」と述べています。
他の開発者がソフトウェアのテストを真剣に受け止める前に見たいと思っていることも、テストを書き始めるもう1つの理由です。
テストはチームが協力するのに役立ちます
これまでのポイントは、アプリケーションを保守する1人の開発者の観点から書かれています。 複雑なアプリケーションはチームによって維持されます。 テストは、同僚が誤ってコードを壊さないことを保証します(そして、あなたが知らないうちに彼らのコードを壊さないことを保証します)。 Djangoプログラマーとして生計を立てたいのであれば、テストを書くのが上手でなければなりません!
基本的なテスト戦略
ライティングテストに取り組む方法はたくさんあります。
一部のプログラマーは、「テスト駆動開発」と呼ばれる分野に従います。 コードを書く前に、実際にテストを書きます。 これは直感に反しているように見えるかもしれませんが、実際には、ほとんどの人がとにかくよく行うことと似ています。問題を説明し、それを解決するためのコードを作成します。 テスト駆動開発は、Pythonテストケースで問題を形式化します。
多くの場合、テストの初心者はコードを作成し、後でテストを行う必要があると判断します。 おそらく、以前にいくつかのテストを作成したほうがよいでしょうが、開始するのに遅すぎることはありません。
テストの作成をどこから始めればよいかわからない場合があります。 数千行のPythonを作成した場合、テストするものを選択するのは簡単ではないかもしれません。 このような場合、新しい機能を追加するとき、またはバグを修正するときのいずれかで、次に変更を加えるときに最初のテストを作成することは有益です。
それでは、すぐにそれを実行しましょう。
最初のテストを書く
バグを特定します
幸い、polls
アプリケーションには、すぐに修正できる小さなバグがあります。Question
が最後に公開された場合、Question.was_published_recently()
メソッドはTrue
を返します。日(正しい)だけでなく、Question
のpub_date
フィールドが将来ある場合(これは確かにそうではありません)。
:djadmin: `shell` を使用してバグを確認し、日付が将来の質問のメソッドを確認します。
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
将来のことは「最近」ではないので、これは明らかに間違っています。
バグを明らかにするためのテストを作成する
:djadmin: `shell` で問題をテストするために行ったのは、まさに自動テストでできることなので、それを自動テストに変えましょう。
アプリケーションのテストの従来の場所は、アプリケーションのtests.py
ファイルです。 テストシステムは、名前がtest
で始まるファイル内のテストを自動的に検索します。
polls
アプリケーションのtests.py
ファイルに以下を入力します。
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
ここでは、将来pub_date
でQuestion
インスタンスを作成するメソッドを使用して、 django.test.TestCase サブクラスを作成しました。 次に、was_published_recently()
の出力を確認します。はがFalseである必要があります。
テストの実行
ターミナルでは、テストを実行できます。
次のようなものが表示されます。
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
別のエラー?
代わりに、ここでNameError
を取得している場合は、パート2 のステップを見逃している可能性があります。ここでは、datetime
とtimezone
のインポートを[ X143X] 。 そのセクションからインポートをコピーして、テストを再実行してみてください。
何が起こったのか:
manage.py test polls
はpolls
アプリケーションでテストを探しました- django.test.TestCase クラスのサブクラスが見つかりました
- テスト用の特別なデータベースを作成しました
- テストメソッドを探しました-名前が
test
で始まるもの test_was_published_recently_with_future_question
で、pub_date
フィールドが30日先のQuestion
インスタンスを作成しました- …そして
assertIs()
メソッドを使用して、was_published_recently()
がTrue
を返すことを発見しましたが、False
を返したいと思っていました。
テストは、どのテストが失敗したか、さらには失敗が発生した行を通知します。
バグの修正
問題が何であるかはすでにわかっています。pub_date
が将来ある場合、Question.was_published_recently()
はFalse
を返すはずです。 models.py
のメソッドを修正して、日付も過去の場合にのみTrue
を返すようにします。
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
そして、テストを再度実行します。
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
バグを特定した後、それを公開するテストを作成し、コード内のバグを修正して、テストに合格しました。
将来、アプリケーションで他の多くの問題が発生する可能性がありますが、テストを実行するとすぐに警告が表示されるため、このバグが誤って再導入されることはありません。 アプリケーションのこの小さな部分は、永久に安全に固定されていると見なすことができます。
より包括的なテスト
ここにいる間に、was_published_recently()
メソッドをさらに特定できます。 実際、あるバグを修正する際に別のバグを導入した場合、それは前向きに恥ずかしいことです。
メソッドの動作をより包括的にテストするには、同じクラスにさらに2つのテストメソッドを追加します。
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
そして今、Question.was_published_recently()
が過去、最近、そして未来の質問に対して賢明な値を返すことを確認する3つのテストがあります。
繰り返しになりますが、polls
は最小限のアプリケーションですが、将来複雑になり、他のコードとやり取りする場合でも、テストを記述したメソッドが期待どおりに動作することが保証されています。
ビューをテストする
投票アプリケーションはかなり無差別です。pub_date
フィールドが将来ある質問を含め、すべての質問を公開します。 これを改善する必要があります。 将来pub_date
を設定すると、その時点で質問が公開されますが、それまでは表示されません。
ビューのテスト
上記のバグを修正したとき、最初にテストを記述し、次にそれを修正するためのコードを記述しました。 実際、これはテスト駆動開発の例でしたが、作業をどの順序で行うかは実際には重要ではありません。
最初のテストでは、コードの内部動作に焦点を当てました。 このテストでは、ユーザーがWebブラウザーを介して体験する動作を確認します。
何かを修正する前に、自由に使えるツールを見てみましょう。
Djangoテストクライアント
Djangoは、ビューレベルでコードを操作するユーザーをシミュレートするためのテスト Client を提供します。 tests.py
、または:djadmin: `shell` でも使用できます。
:djadmin: `shell` から始めます。ここでは、tests.py
では必要のないいくつかのことを行う必要があります。 1つ目は、:djadmin: `shell` でテスト環境をセットアップすることです。
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()は、テンプレートレンダラーをインストールします。これにより、response.context
など、他の方法では利用できない応答の追加属性を調べることができます。 このメソッドはテストデータベースをセットアップしないため、既存のデータベースに対して以下が実行され、作成済みの質問によって出力がわずかに異なる場合があることに注意してください。 settings.py
のTIME_ZONE
が正しくない場合、予期しない結果が生じる可能性があります。 以前に設定したことを覚えていない場合は、続行する前に確認してください。
次に、テストクライアントクラスをインポートする必要があります(後でtests.py
で、独自のクライアントに付属する django.test.TestCase クラスを使用するため、これは必要ありません) :
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
準備ができたら、クライアントにいくつかの作業を依頼できます。
>>> # get a response from '/'
>>> response = client.get('/')
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a hardcoded 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/1/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
私たちの見解を改善する
投票のリストには、まだ公開されていない投票が表示されます(つまり、 将来的にpub_date
を持つもの)。 それを修正しましょう。
Tutorial 4 では、 ListView に基づくクラスベースのビューを導入しました。
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()
と比較して日付もチェックするように変更する必要があります。 まず、インポートを追加する必要があります。
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_date
が-以下、つまり-timezone.now
より前のQuestion
を含むクエリセットを返します。
新しいビューをテストする
これで、runserver
を起動し、ブラウザにサイトをロードし、過去と未来の日付でQuestions
を作成し、過去と未来の日付のみをチェックすることで、これが期待どおりに動作することを確認できます。公開済みが一覧表示されます。 このに影響を与える可能性のある変更を行うたびに、を実行する必要はありません。したがって、:djadmin: `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.
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past 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.
"""
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: Past question.>']
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
create_question(question_text="Past question 1.", days=-30)
create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)
これらのいくつかをもっと詳しく見てみましょう。
1つ目は、質問の作成プロセスから繰り返しを取り除くための質問ショートカット機能create_question
です。
test_no_questions
は質問を作成しませんが、「利用可能な投票はありません」というメッセージを確認します。 latest_question_list
が空であることを確認します。 django.test.TestCase クラスは、いくつかの追加のアサーションメソッドを提供することに注意してください。 これらの例では、 assertContains()および assertQuerysetEqual()を使用します。
test_past_question
で、質問を作成し、リストに表示されることを確認します。
test_future_question
では、将来的にpub_date
で質問を作成します。 データベースはテストメソッドごとにリセットされるため、最初の質問は存在しなくなります。したがって、インデックスに質問が含まれることはありません。
等々。 実際、テストを使用して、サイトでの管理者の入力とユーザーエクスペリエンスのストーリーを伝え、すべての状態で、システムの状態の新しい変更ごとに、期待される結果が公開されていることを確認しています。
DetailViewのテスト
私たちが持っているものはうまく機能します。 ただし、将来の質問が index に表示されなくても、ユーザーは正しいURLを知っているか推測していれば、質問にアクセスできます。 したがって、DetailView
に同様の制約を追加する必要があります。
class DetailView(generic.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
が表示されるテストを追加します。いいえ:
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)
より多くのテストのためのアイデア
同様のget_queryset
メソッドをResultsView
に追加し、そのビューの新しいテストクラスを作成する必要があります。 これは、先ほど作成したものと非常によく似ています。 実際、多くの繰り返しがあります。
他の方法でアプリケーションを改善し、途中でテストを追加することもできます。 たとえば、[X21X] がないサイトでQuestions
を公開できるのはばかげています。 したがって、私たちの見解はこれをチェックし、そのようなQuestions
を除外することができます。 私たちのテストでは、Choices
なしでQuestion
を作成し、それが公開されていないことをテストし、同様のQuestion
と Choices
を作成します。 ]、がで公開されていることをテストします。
おそらく、ログインしている管理者ユーザーは、未公開のQuestions
を表示できるようにする必要がありますが、通常の訪問者は表示できません。 繰り返しますが、これを達成するためにソフトウェアに追加する必要があるものはすべて、テストを伴う必要があります。最初にテストを記述してからコードをテストに合格させるか、コード内のロジックを最初に実行してからテストを記述します。証明する。
ある時点で、テストを見て、コードがテストの肥大化に苦しんでいるかどうか疑問に思う必要があります。これにより、次のことが可能になります。
テストするときは、多いほど良いです
私たちのテストは制御不能になっているように見えるかもしれません。 このレートでは、すぐにアプリケーションよりもテストに多くのコードが含まれるようになり、残りのコードのエレガントな簡潔さに比べて、繰り返しは見栄えが悪くなります。
関係ありません。 それらを成長させます。 ほとんどの場合、一度テストを書いて、それを忘れることができます。 プログラムの開発を続けると、便利な機能を実行し続けます。
テストを更新する必要がある場合があります。 Questions
とChoices
のみが公開されるようにビューを修正するとします。 その場合、既存のテストの多くは失敗します-どのテストを最新の状態にするために修正する必要があるかを正確に教えてくれます。その範囲で、テストは自分自身の面倒を見るのに役立ちます。
最悪の場合、開発を続けると、現在冗長になっているテストがあることに気付くかもしれません。 それでも問題はありません。 冗長性のテストでは、良いものです。
テストが適切に配置されている限り、テストが管理不能になることはありません。 良い経験則には、次のものが含まれます。
- モデルまたはビューごとに個別の
TestClass
- テストする条件のセットごとに個別のテスト方法
- それらの機能を説明するテストメソッド名
さらなるテスト
このチュートリアルでは、テストの基本の一部のみを紹介します。 あなたができることはもっとたくさんあり、いくつかの非常に賢いことを達成するためにあなたが自由に使える非常に便利なツールがたくさんあります。
たとえば、ここでのテストではモデルの内部ロジックの一部とビューが情報を公開する方法について説明しましたが、 Selenium などの「ブラウザー内」フレームワークを使用してHTMLの方法をテストできます。実際にはブラウザでレンダリングします。 これらのツールを使用すると、Djangoコードの動作だけでなく、JavaScriptなどの動作も確認できます。 テストがブラウザを起動し、人間がそれを運転しているかのようにサイトとの対話を開始するのを見るのはかなりのことです! Djangoには、Seleniumなどのツールとの統合を容易にする LiveServerTestCase が含まれています。
複雑なアプリケーションを使用している場合は、継続的インテグレーションの目的で、コミットごとにテストを自動的に実行して、品質管理自体が(少なくとも部分的に)自動化されるようにすることができます。
アプリケーションのテストされていない部分を見つける良い方法は、コードカバレッジを確認することです。 これは、壊れやすいコードやデッドコードを特定するのにも役立ちます。 コードの一部をテストできない場合は、通常、コードをリファクタリングまたは削除する必要があることを意味します。 カバレッジは、デッドコードを特定するのに役立ちます。 詳細については、 Coverage.py との統合を参照してください。
Djangoでのテストには、テストに関する包括的な情報があります。
次は何ですか?
テストの詳細については、 Djangoでのテストを参照してください。
Djangoビューのテストに慣れたら、このチュートリアルのパート6 を読んで、静的ファイル管理について学習してください。