[前回] Django+Reactで学ぶプログラミング基礎(10): Djangoチュートリアル(投票アプリその5-1)
はじめに
Django公式チュートリアル、その5-2です。
前回は、DjangoのAPIを駆使し、動作確認を行いました。
今回は、超重要な自動テストについてです。
Djangoアプリ作成(その5-2): 投票(poll)アプリ
今回の内容
- 自動テスト
自動テストの導入
自動テストとは
- テストは、コードの動作をチェックするルーチン
- 自動テストは、テスト作業がシステムによって実行されること(手動でなく)
- 一度テストセットを作成すると、
- アプリに変更を加えるたびに、
- 意図した通りにコードが動作するか確認できる
- アプリに変更を加えるたびに、
- 一度テストセットを作成すると、
なぜテストを作成する必要があるか
- ①テストはあなたの時間を節約
- 高機能アプリでは、コンポーネント間の複雑な相互作用が多数存在
- コンポーネントを変更した場合、動作確認のため様々なテストデータを用いてプログラムを走らせる必要あり
- 自動テストを導入することで、プログラムの動作確認をを一瞬で終わらせることができる
- プログラムのどこで予期せぬ動作が起きたかを見極めるのに役立つ
- ②問題点検出のみならず、問題発生を防ぐ
- テストなくしては、アプリの目的や意図した動作が曖昧になってしまう
- 自分自身で書いたコードであっても、コードがすることを正確に理解するのに時間がかかってしまうことがあり
- テストにより、自分自身では気づかなかった間違いを見つけ出してくれる
- ③コードをより魅力的にする
- テストをしていないアプリは信用できず、だれも使ってくれない
- Djangoを開発した
Jacob Kaplan-Moss
氏よりテストのないコードは、デザインとして壊れている
- Djangoを開発した
- ソフトウェアを他の開発者に真剣に見てもらうため
- テストを書くべき
- テストをしていないアプリは信用できず、だれも使ってくれない
- ④チームの共同作業に役立つ
- 複雑なアプリはチームでメンテナンスされる
- テストは、誰かのうっかりミスによりコードを壊さないように守ってくれる
- Djangoプログラマとして生きてゆくには、絶対に良いテストを書かなければならない
基本的なテスト方針とアプローチ
- テスト駆動開発
- 実際にコードを書く前にテストを書く
- 問題をきちんと言葉にしてから、その問題を解決するためのコードを書く
- テスト駆動開発は、問題をPythonのテストケースとして形式化
- どこからテストを書き始めるべきか
- 新しい機能追加やバグ修正を行う時
早速、テストを作成
バグ、みーつっけ
-
Question.was_published_recently()
メソッド- (OK)
Question
が昨日以降に作成された場合、True
を返す - (NG)
Question
のpub_date
が未来の日付になっている場合も、True
を返す
- (OK)
-
shellを使用し、メソッドのバグを再現確認
C:\kanban\pollsite>..\venv\.venv\Scripts\activate
(venv) C:\kanban\pollsite>python manage.py shell
- 必要モジュールをインポート
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
-
pub_date
値が今日から30日後となるように、Question
インスタンスを作成
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
-
was_published_recently()
メソッドを呼び出す- 未来の日付なのに、最近公開されたとうそを言っている
>>> future_question.was_published_recently()
True
>>> exit()
バグをあぶり出すため、テストを作成
- 上述の手動によるバグ確認を、自動テストに変換
- アプリケーションのテストを書く場所
-
tests.py
ファイル - テストシステムが、名前が
test
で始まるファイルから、自動的にテストを見つけてくれる
-
-
django.test.TestCase
を継承したサブクラスを作成- 未来の日付の
pub_date
を持つQuestion
のインスタンスを生成するメソッドを作成 -
was_published_recently()
の出力をチェック- 期待値
False
と一致するか
- 期待値
- 未来の日付の
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)
-
polls/tests.py
を保存
テストを実行
(venv) C:\kanban\pollsite>python manage.py test polls
Found 1 test(s).
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)
was_published_recently() returns False for questions whose pub_date
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\kanban\pollsite\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
- テストが失敗し、失敗が起こったコードの行数まで教えてくれる
- 上記テストでやっていること
-
manage.py test polls
は、pollsアプリ内にあるテストを探す -
django.test.TestCase
クラスのサブクラスを発見 - テスト用のデータベースを作成
-
test
で始まるテスト用メソッドを探す -
test_was_published_recently_with_future_question
の中で- pub_dateフィールドが、今日から30日後の日付となる
Question
インスタンスを作成
- pub_dateフィールドが、今日から30日後の日付となる
-
assertIs()
メソッドを使用し、以下二つが一致するか評価- 期待値:
False
-
was_published_recently()
の返却値:True
- 期待値:
-
バグを修正
- 問題の原因
-
pub_date
が未来の日付の場合-
Question.was_published_recently()
メソッドが、間違ってTrue
を返している(正しくはFalse
) - メソッドを修正し、日付が過去だった場合にのみ
True
を返すように
-
-
polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
再度、テストを実行
(venv) C:\kanban\pollsite>python manage.py test polls
Found 1 test(s).
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'...
バグを修正したので、テストが正常に終わりました。
より包括的なテスト
- 一つのバグを修正する際、別のバグを作り出す可能性あり
- 対処: メソッドの振る舞いをより包括的にテスト
- 同じクラスにさらに2つのテストを追加
- 対処: メソッドの振る舞いをより包括的にテスト
polls/tests.py
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()
が、考えられるすべてのケースに対し、チェック実施- 過去質問
- 現在質問
- 未来質問
- メソッドに対し、テストを書いたおかげで
- 将来、このアプリがどんなに複雑になっても
- または、他のどんなコードと相互作用するようになっても
- メソッドが期待どおり動作することを保証できる
おわりに
自動テストがもたらすメリットを理解しました。
テストの作成、本当に大事ですね。
次回も続きます。お楽しみに。