Django公式 チュートリアル その5
関連記事
- Django 仮想環境からインストールまで
- Django 公式チュートリアル
- Django公式 チュートリアル その1
- Django公式 チュートリアル その2
- Django公式 チュートリアル その3
- Django公式 チュートリアル その4
- Django公式 チュートリアル その5
- Django公式 チュートリアル その6
(個人的な学習ノートです)
- 関連記事
- 関連記事
- 学習環境
- 参考サイト
- あとは努力あるのみ・・・
関連記事
そのままチュートリアルに進みます!
「はじめてのDjangoアプリ作成 その5」
学習環境
macOS Sierra 10.12
python3.6
Django1.11
XAMPP
MySQL(MariaDB10.1.21)
テキストエディッタ:ATOM
参考サイト
はじめての Django アプリ作成、その 5 | Django documentation | Django
上記サイトに沿って進めます。
あとは努力あるのみ・・・
そろそろ終わりは見えてきました。
他の人にできて自分にできないのは・・・
努力が足りないから。
人は隠れて努力しています。
そして私も隠れて努力しようと思います!
自動テストの導入
自動テストって何よ!?
テストとは、コードの動作を確認する単純なプログラム。
テストは異なるレベルで実行される。
自動テストは、テスト作業がシステムによって実行されること。
一度テストセットを作成すると、それからはアプリに変更を加えるたびに、あなたの意図した通りにコードが動作するか確認が可能。
手動でテストする時間がかかることはありません。
なぜテストを作成せねばならないだべさ?
どうしてテストを作るのか?また、いつやるのか?(今でしょ!)
これまで学んだ Python/Django の知識に満足し、さらに別のことを学ぶのは大変で不必要なことだと思われるかもしれません。
だって投票アプリケーションはきちんと動いているし、わざわざ自動テストを導入したところでアプリケーションがより良くなるわけではないのだから。
もし Django プログラミングを学ぶことの全ての目的がこの投票アプリケーションを作ることであるのならば確かに自動テストの導入は必要ないと思います。しかし、そうではないのならば自動テストについて学ぶことは役に立つことでしょう。
テストはあなたの時間を節約すルンです
ある一定の基準まで、動くであろうことを確認することがテスト。
高機能なアプリケーションでは、コンポーネント間で複雑な連携が数多くあるかもしれません。
プログラムの変更によって予想だにしない箇所の挙動が変わってしまう可能性がある。
それを確かめるためには、様々なテストデータを用いてプログラムを用いて 『正しく動いていそう』 であることを確認する必要があります - これは効率がよくありません。
自動テストを導入することによってプログラムが正しく動くことの確認を一瞬で終わらせることができ、またテストはプログラムのどこで予期せぬ動作が起きたかを見極めるのに役立足せることができる。
テストを書くという行為は、特にプログラムが適切に動くと分かっているときには、生産的でも創造的でもないつまらないことのように思われるかもしれない。
しかしテストを書くことは、何時間もかけてアプリケーションの動作を確認したり、新しく発生した問題の原因を探したりすることよりもずっとやりがいのあることなはず。
テストは問題点を検出するのみならず、問題が発生するのを防ぐ
テストを単に開発の負な面と考えることは誤り。
テストなくしては、アプリケーションの目的や意図した動作というものが曖昧になってしまうこともある。
自分自身で書いたコードであっても、時にはそのコードがすることを正確に理解するのに時間がかかってしまうことがあります。
テストはこの状況を大きく変え、いわばコードを内側から照らし出してくれる。
そして、何か間違ったことをしてしまった時には、自分自身では間違っていると気づかなかった場合でさえ、間違いが起きた場所にスポットライトを当ててくれるのです。
テストは、コードをより魅力的にします
Django を開発した Jacob Kaplan-Moss は次の言葉を残している。
「テストのないコードは、デザインとして壊れている。」
あなたのソフトウェアを他の開発者が真剣に見てもらうというのも、テストを書くべきもう一つの理由です。
テストを書くことはチームで共同作業を行う上で役に立つ
これまでの点は、1人の開発者でアプリケーションをメンテナンスしているという観点から書きました。
しかし、複雑なアプリケーションはチームでメンテナンスされるようになるものです。
テストは、あなたが書いたコードを他人がうっかり壊してしまうことから守ってくれます (そして、他の人が書いたコードをあなたが壊してしまうことからも)。
基本的なテスト方針
テストを書くためのアプローチには、さまざまなものがあります。
プログラマの中には、「テスト駆動開発」の原則に従っている人がいます。
これは、実際にコードを書く前にテストを書く、という原則です。
この原則は直感に反するように感じるかもしれませんが、実際には多くの人がどんなことでも普通にしていることに似ています。
つまり、問題をきちんと言葉にしてから、その問題を解決するためのコードを書く、ということです。
テスト駆動開発は、ここで言う問題を単に Python のテストケースとして形式化しただけのことです。
テストの初心者の多くは、先にコードを書いてから、その後でテストが必要だと考えるものです。
おそらく、おそらく早くからいくつかテストを書いておいた方が良いですが、テストを始めるのに遅すぎるということはありません。
どこからテストを始めるべき場所を見つけるのが難しいこともあります。
もしすでに数千行の Python コードがあったとしたら、テストすべき場所を選ぶのは簡単ではないかもしれません。
そのような場合には、次に新しい機能やバグの修正を行う時に、最初のテストを書いてみると役に立つでしょう。
それでは早速始めてみましょう。
初めてのテスト作成
バグを見つけたとき運よく、 polls のアプリケーションにはすぐに修正可能な小さなバグがありました。Question.was_published_recently() のメソッドは Question が昨日以降に作成された場合に True を返すのですが(適切な動作)、 Question の pub_date が未来の日付になっている場合にも「True「を返してしまいます(不適切な動作)。 このバグが本当に存在するのかを確かめるために、 shell: から未来の日付の Question を作成し、メソッドの結果を見てみましょう:
>>> import datetime
>>> from django.utils import timezone>>> from polls.models import Question
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True
未来の日付は 『最近』 ではないため、この結果は明らかに間違っている
バクを明らかにするためテストを作成
問題をテストするために shell でたった今したことこそ、自動テストでしたいことです。
そこで、それを自動テストの中に取り込みましょう。
アプリケーションのテストを書く場所は、慣習として、アプリケーションの tests.py ファイル内ということになっています。
テストシステムが test で始まる名前のファイルの中から、自動的にテストを見つけてくれます。
polls アプリケーションの tests.py ファイルに次のコードを書きます。
( polls/tests.py )
import datetime from django.utils import timezone from django.test import TestCase from .models import Question class QuestionMethodTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently() should return 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)
ここではまず、django.test.TestCase を継承したサブクラスを作り、未来の日付の pub_date を持つ Question のインスタンスを作る。
それから、was_published_recently() の出力をチェック。
これは False になるはずです。
テストの実行
ターミナルから、次のコマンドでテストが実行できます。
$ python manage.py test polls
次のような結果となる
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/kenta/Desktop/django/DjangoTest/mysite/polls/tests.py", line 18, 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.002s
FAILED (failures=1)
Destroying test database for alias 'default'...
ここでは以下のようなことを行なっています。
- python manage.py test polls は polls アプリケーションからテストを探す
- django.test.TestCaseクラスのサブクラスを発見
- テストのための特別なデータベースを作成
- テスト用のメソッドとして、test で始まるメソッドを探す
- test_was_published_recently_with_future_question の中で、pub_date フィールドに今日から30日後の日付を持つ Question インスタンスが作成
- そして最後に、 assertIs() メソッドを使うことで、本当に返してほしいのは False だったにもかかわらず、 was_published_recently() が True を返していることを発見
テストは私たちにテストの失敗を教えてくれるだけでなく、失敗が起こったコードの行数まで教えてくれる。
バグを修正
私たちはすでに問題の原因を知っています。
それは、Question.was_published_recently() は pub_date が未来の日付だった場合には False を返さなければならない、ということ。
models.py にあるメソッドを修正して、日付が過去だった場合にのみ True を返すように変更。
( polls/models.py )
from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils import timezone import datetime @python_2_unicode_compatible class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __str__(self): return self.question_text # 修正したメソッド def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now @python_2_unicode_compatible class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text
再度テストを実施
$ python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
Destroying test database for alias 'default'...
バグを発見した後、私たちはそのバグをあぶり出してくれるようなテストを書いて、コード内のバグを直したので、テストは無事にパスしました。
このアプリケーションでは将来、たくさんの他のバグが生じるかもしれませんが、このバグがうっかり入ってしまうことは二度とありません。
単にテストを実行するだけで、すぐに警告を受けられるからです。
アプリケーションのこの小さな部分が、安全に、そして永遠にピン留めされたと考えて差し支えありません。
より包括的なテスト
この段階で、was_published_recently() メソッドをさらにピン留めしておけます。
実際、一つのバグを直したことでほかのバグを作り出すなんてしたくありませんので。
このメソッドの振る舞いをより包括的にテストするために、同じクラスにさらに2つのテストを追加してみます。
( polls/tests.py )
def test_was_published_recently_with_old_question(self): """ was_published_recently() should return False for questions whose pub_date is older than 1 day. """ time = timezone.now() - datetime.timedelta(days=30) 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() should return True for questions whose pub_date is within the last day. """ time = timezone.now() - datetime.timedelta(hours=1) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True)
これで、Question.was_published_recently() が過去、現在、そして未来の質問に対して意味のある値を返すことを確認する3つのテストが揃う。
polls は簡単なアプリケーションであっても、将来このアプリケーションが他のどんなコードと関係するようになっても、複雑さは増していく。
メソッドに対してテストを書いたおかげで、今ではそのメソッドが期待したとおりに動作することを、ある程度保証でききるようになったことになる。
ビューをテストする
この投票アプリケーションは、まだ質問をちゃんと見分けることができない。
pub_date フィールドが未来の日付になっている質問を含め、どんな質問でも公開してしまいます。
この点を改善するべきでしょう。
pub_date を未来に設定するということは、その Question がその日付になった時に公開され、それまでは表示されないことを意味する。
ビューに対するテスト
Django テストクライアント
Django は、ビューレベルでのユーザとのインタラクションをシミュレートすることができる Clientを用意している。
これを tests.py の中や shellでも使うことができる。
もう一度 shellを使って見る。
ここでテストクライアントを使う場合には、tests.py では必要がない2つの準備が必要になる。
まず最初にしなければならないのは、shell の上でテスト環境をセットアップすること。
$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()は、レンダリングのテンプレートをインストール。
これによって、 response.context を含むような、レスポンス上のいくつかの追加的な属性を試験が可能になる。
注意点として、データベースを作成しないので、既存のデータベースに対して以下が実行され、あなたの作成した question によってはアウトプットが多少異なるかもしれまない。
また settings.py の TIME_ZONE が正しくない場合は、予期しない結果になる。
設定変更がまだの場合は、次に進む前に設定を確認してください。
つぎに、テストクライアントのクラスをインポートする必要がある (後ほどの tests.py の中では、」class」django.test.TestCase クラス自体がクライアントを持っているため、インポートは不要)。
>>> response = client.get('/')
Not Found: /
>>> response.status_code
404
>>> 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「 の日付が未来になっている) 投票が表示される状態になっている。
これを直しましょう。
チュートリアル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() と比較して確認できるようにする必要があります。まず、インポート文を追加します:
そして、次のように 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 を実行して、ブラウザでサイトを読み込む。
過去と未来、それぞれの日付を持つ Question を作成し、すでに公開されている質問だけがリストに表示されるかどうかを確認。
この通りにちゃんと動作しているか、プロジェクトにわずかでも変更を加えるたびに毎回手動で 確認したいなどとは思わないですよね?
それなら、今回も上の shellのセッションに基づいてテストを作ります。
まず、polls/tests.py に次の行を追加します。
( polls/tests.py )
from django.urls import reverse
そして、question を簡単に作れるようにするショートカット関数と、新しいテストクラスを作成。
def create_question(question_text, days): """ Creates 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 QuestionViewTests(TestCase): def test_index_view_with_no_questions(self): """ If no questions exist, an appropriate message should be 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_index_view_with_a_past_question(self): """ Questions with a pub_date in the past should be 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_index_view_with_a_future_question(self): """ Questions with a pub_date in the future should not be 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_index_view_with_future_question_and_past_question(self): """ Even if both past and future questions exist, only past questions should be 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_index_view_with_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.>'] )
これらのコードを詳しく見ていきましょう。
- question のショートカット関数 create_question 。この関数は、処理の中の question を作成する繰り返しを取り除いてくれています。
- test_index_view_with_no_questions は question を1つも作らないが、「No polls are available.」 というメッセージが表示されていることをチェックし、latest_question_list が空になっているか確認。django.test.TestCaseクラスが追加のアサーション (assertion) メソッドを提供していることに注意。
- チュートリアルでは、assertContains() と assertQuerysetEqual() を使用。
- test_index_view_with_a_past_question では、question を作成し、その question がリストに現れるかどうかを検証。
- test_index_view_with_a_future_question では、pub_date が未来の日付の質問を作成。データベースは各テストメソッドごとにリセットされるので、この時にはデータベースには最初の質問は残っていない。そのため、index ページにはquestion は1つもない。
以下のテストメソッドも同様です。実際のところ、私たちはテストを用いて、管理者の入力とサイトでのユーザの体験についてのストーリを語り、システムの各状態とそこでの新しい変化のそれぞれに対して、期待通りの結果が公開されているかどうかをチェックしている。
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())
2つのテストを追加。
pub_date が過去の Question が表示されることを確認するテストと、pub_date が未来の Question が表示されないことを確認するテスト。
( polls/tests.py )
class QuestionIndexDetailTests(TestCase): def test_detail_view_with_a_future_question(self): """ The detail view of a question with a pub_date in the future should return 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_detail_view_with_a_past_question(self): """ The detail view of a question with a pub_date in the past should display 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)
さらなるテストについて考える(もーいいよw)
ResultsView にも同じように get_queryset メソッドを追加して、新しいテストクラスも作らなければならないようです。
しかしこれは、今作ったばかりのものとそっくりになるでしょう。実際、テストは重複だらけになるはずです。
テストを追加することによって、同じように他の方法でアプリを改善できるでしょう。
例えば、Choices を一つも持たないQuestions が公開可能になっています。
このような Questions を排除するようビューでチェックできます。
Choices がない Question を作成し、それが公開されないことをテストし、同じようにして、Choices がある Question を作成し、それが公開 される ことをテストすることになるでしょう。
もしかすると管理者としてログインしているユーザーは、一般の訪問者と違い、 まだ公開されていない Questionsを見ることができるようにした方がいいかもしれません。
また繰り返しになりますが、この問題を解決するためにソフトウェアにどんなコードが追加されべきであったとしても、そのコードにはテストが伴うべきです。
テストを先に書いてからそのテストを通るコードを書くのか、あるいはコードの中で先にロジックを試してからテストを書いてそれを検証するのか、いずれにしてもです。
ある時点で、書いたテストが限界に達しているように見え、テストが膨らみすぎてコードが苦しくなってしまうのではないかという疑問が浮かんでくるでしょう。
こうなって場合にはどうすれば良いのでしょうか?
テストにおいて、多いことはいいことだ
私たちのテストは、手がつけられないほど成長してしまっているように見えるかもしれません。
この割合で行けば、テストコードがアプリケーションのコードよりもすぐに大きくなってしまうでしょう。
そして繰り返しは、残りの私たちのコードのエレガントな簡潔さに比べて、美しくありません。
構いません。
テストコードを大きくしましょう。
たいていあなたはテストを一回書いてそのことを忘れます。
プログラムを開発し終えるまで便利な関数を使いそれを続けましょう。
時には、テストにアップデートが必要になることがあります。
Choices を持つ Questions だけを公開するよう、ビューを修正したときのことを思い出してください。
この後、既存のテストの多くは失敗します。
この失敗は、どのテストが、最新の状態に対応するために修正する必要があるのか をわたしたちに教えてくれているのです。
テストはこの意味でも、テスト自身をチェックするのに役に立っています。
最悪の場合、開発を続けていくにつれて、あるテストが今では冗長なものになっていることに気づいてしまうかもしれません。
これも問題ではありません。
テストにおいては、冗長であることは 良い ことなのです。
テストを意味のあるものとなるように整えている限り、手に負えないものになることはありません。
経験上、次のルールを守るようにすれば問題ありません。
-
モデルやビューごとに TestClass を分割する
-
テストしたい条件の集まりのそれぞれに対して、異なるテストメソッドを作る
-
テストメソッドの名前は、その機能を説明するようなものにする
さらなるテスト
このチュートリアルでは、テストの基本の一部を紹介しました。
この他にもあなたにできることはまだまだたくさんありますし、いろいろと賢いことを実現するに使えるとても便利なツールが数多く用意されています。
たとえば、ここでのテストでは、モデルの内部ロジックと、ビューの情報の公開の仕方をカバーしましたが、ブラウザが HTML を実際にどのようにレンダリングのするのかをテストする Seleniumのような 「in-browser」 のフレームワークを使うこともできます。
これらのツールは、Django が生成したコードの振る舞いだけでなく、たとえば、 JavaScript の振る舞いも確認できます。
テストがブラウザを起動してサイトとインタラクションしているのを見るのはとても面白いですよ。
まるで本物の人間がブラウザを操作しているかのように見えるんです!
Django には、Selenium のようなツールとの連携を容易にしてくれる LiveServerTestCaseが用意されています。
複雑なアプリケーションを開発する時には、継続的インテグレーション (continous integration) のために、コミットの度に自動的にテストを実行したいくなるかもしれませんね。
継続的インテグレーションを行れば、品質管理それ自体が、少なくとも部分的には自動化できます。
アプリケーションのテストされていない部分を発見するには、コードカバレッジをチェックするのが良いやり方です。
これはまた、脆弱なコードや使用されていないデッドコードの発見にも役に立ちます。
テストできないコード片がある場合、ふつうは、そのコードはリファクタリングするか削除する必要があることを意味します。
カバレッジはデッドコードの識別に役に立つでしょう。
次回は、「初めてのDjangoアプリ作成、その6」に 取り掛かかります。
だんだんコピペだらけになってきたな・・・。
覚えるのと後で見返すために形に残してきましたが・・・。