
【第5回】Responderを使ってDjangoチュートリアルをやってみた【自動テスト導入編】
2021.12.20
第5回~Responderを使ってDjangoチュートリアルをやってみた~
前回の記事の「【第4回】Responderを使ってDjangoチュートリアルをやってみた【公開ビュー作成編】」の続きです。
今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。
Django のチュートリアル「はじめての Django アプリ作成」を、Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。
【はじめての Django アプリ作成】
https://docs.djangoproject.com/ja/2.2/intro/
第1回はこちら
自動テストとは?
この記事は、「はじめての Django アプリ作成、その5 」に沿って書かれており、「テスト」について言及されています。
テストとは、自分で書いたコードが正しく動作するかを確認するために行います。
小さなもの(あるひとつの関数メソッド)に行われる場合もあれば、ソフトウェア全体に対して、ユーザが行うであろう一連の動作に対して期待通りの出力が得られるかを確認するために行われる場合もあります。
今までやっていた作業とは少し毛色が違いますが、飛ばさずに実際に手を動かしてみましょう!
なぜテストを作成する必要があるのか?
この記事で作成したPollsアプリケーションは、一見、投票アプリとして正しく動作しているように見えます。
では、なぜテストを作成しなければならないのでしょうか?
テストは時間節約につながる
「実際にアプリを動かしながら、正しく動いているか確認する」というのもテストとしては十分です。
しかし、アプリケーションが複雑になればなるほど、各機能の相互作用が複雑になっていきます。
ある部分を変更したら、他の部分の結果が変わるかもしれません。
そのたびに、実際にアプリを立ち上げて実際に手を動かしてテストするのは時間の無駄でしょう。
色々なテストをあらかじめ作成しておけば、変更のたびに「たったひとつの命令」を実行するだけでバグを炙り出せるのです。
テストはチーム開発に役立つ
大きなアプリケーションになると、単独開発とはいかないことでしょう。
複数人で開発することは、他人の書いたコードのメンテナンスに多くの時間を使ったりと、困難なことも多いです。
しかし、テストコードがあると、それを実行するだけでどこで問題が発生しているのかを内面から照らしてくれます。
それゆえに、プログラマとして良いテストコードを書けるようになる必要があります。
はじめてのテスト作成
ということで、この記事でテストコードを書く練習をしていきましょう!
運良く、このPollsアプリケーションには、すぐに修正可能な小さなバグがありました。
Questionクラスの was_published_recently()は、「最近の質問」に対して True を返す(適切な動作)メソッドです。
しかし、未来の日付に対しても True を返してしまいます。(不適切な動作)
未来の日付は「最近ではない」ので、この結果は明らかに間違っています。
それでは、このバグに対してテストを作成してみましょう。
テストを作成
新しく、 test_sample.py を作成し、以下の内容を書いて保存してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | """ test_question.py サンプルテストケース """ import pytest import run as myApp from datetime import datetime, timedelta from models import Question @pytest.fixture def api(): return myApp.api class TestQuestionModel: def test_was_published_recently_with_future_question(self, api): """ 未来の質問に対してwas_published_recently()はFalseを返すはずである :param api: :return: """ # 未来の公開日となる質問を作成 time = datetime.now() + timedelta(days=30) feature_question = Question('future_question', pub_date=time) # これはFalseとなるはず assert feature_question.was_published_recently() is False |
Responderでは、テストはサポートしていないので、pytestと呼ばれるモジュールを使用します。(公式でも推奨)
したがって、 import pytest を記述します。
その次に、実際にアプリを実行するファイルをインポート( import run as myApp)します。
ここでは、分かりやすくするため、名前を myApp としてインポートしています。
1 2 3 | @pytest.fixture def api(): return myApp.api |
そして、ResponderのAPIをインスタンス化しておきます。
その下に実際にテストする内容を書きます。
- クラス名は class TestXXX
- メソッド名は def test_XXX(api)
基本的に、クラス名は class TestXXX 、メソッド名は def test_XXX(api) としましょう。
ある関数に対するものなら、XXXは関数名の方が望ましいです。
実行
それでは、実行してみます。
1 | User@User:responder$ pytest |
すると、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | ============================= test session starts ============================== platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 rootdir: /Users/araki/PycharmProjects/responder collected 1 item test_question.py F [100%] =================================== FAILURES =================================== ______ TestQuestionModel.test_was_published_recently_with_future_question ______ self = <test_question.TestQuestionModel object at 0x105d341d0> api = <responder.api.API object at 0x104362e90> def test_was_published_recently_with_future_question(self, api): """ 未来の質問に対してwas_published_recently()はFalseを返すはずである :param api: :return: """ # 未来の公開日となる質問を作成 time = datetime.now() + timedelta(days=30) feature_question = Question('future_question', pub_date=time) # これはFalseとなるはず > assert feature_question.was_published_recently() is False E assert True is False E + where True = <bound method Question.was_published_recently of <models.Question object at 0x105d34f10>>() E + where <bound method Question.was_published_recently of <models.Question object at 0x105d34f10>> = <models.Question object at 0x105d34f10>.was_published_recently test_question.py:30: AssertionError =============================== warnings summary =============================== # ~~~ 省略 ~~~ # -- Docs: https://docs.pytest.org/en/latest/warnings.html ===================== 1 failed, 3 warnings in 0.56 seconds ===================== |
このようにテスト結果が返ってきます。
返ってきた内容は以下のようになります。
- ディレクトリ内のテストファイルを探す
- テストファイルを実行し、成功したか test_sample.py F
- 失敗したテストと、原因となる処理
バグを修正する
問題の場所が特定でき、バグへの対処法も分かっています。
未来の日付のときは、 False を返すように修正しましょう。
1 2 3 4 5 6 7 8 9 | class Question(Base): def was_published_recently(self, days=1): """ 最近追加された質問に対してTrueを返す関数 :return: """ now = datetime.now() return now - timedelta(days=days) <= self.pub_date <= now |
実行
そうしたら、もう一度テストを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | ============================= test session starts ============================== platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 rootdir: /Users/araki/PycharmProjects/responder collected 1 item test_question.py . [100%] =============================== warnings summary =============================== # ~~~ 省略 ~~~ # -- Docs: https://docs.pytest.org/en/latest/warnings.html ===================== 1 passed, 3 warnings in 0.44 seconds ===================== |
テストにpassしました!
(Warning はとりあえず今はスルー)
小さなテストコードですが、今後アプリケーションが複雑になったとしても、期待通りの動作をするかどうかを簡単にテストできるようになりました。
より包括的なテスト
ついでに、他のテストケースも書いてみます。
今からやるのは、いわゆる境界値テストというものです。
入力された数値やデータが何かしらの閾(しきい)値によって、出力が異なるような処理を書いた場合、その閾値付近でテストを行います。
例えば、以下のようなテストが良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def test_was_published_recently_with_boundary_question(self, api): """ == 境界値テスト == 1日1秒前の質問に対してはwas_published_recently()はFalseを返すはずである また,23時間59分59秒以内であればwas_published_recently()はTrueを返すはずである :param api: :return: """ # 最近の境界値となる質問を作成 time_old = datetime.now() - timedelta(days=1) time_res = datetime.now() - timedelta(hours=23, minutes=59, seconds=59) old_question = Question('old_question', time_old) res_question = Question('resent_question', time_res) assert old_question.was_published_recently() is False assert res_question.was_published_recently() is True |
1 2 3 4 5 6 7 8 9 10 11 12 13 | ============================= test session starts ============================== platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 rootdir: /Users/araki/PycharmProjects/responder collected 2 items test_question.py .. [100%] =============================== warnings summary =============================== # ~~~ 省略 ~~~ # -- Docs: https://docs.pytest.org/en/latest/warnings.html ===================== 2 passed, 3 warnings in 0.47 seconds ===================== |
これで、Questionテーブルの公開日に対しては、過去、現在、未来すべてのテストが完成しました。
その他バグ取り
今、作っている投票アプリケーションは、まだ質問を適切に見分けることができません。
pub_dateフィールドが、未来の日付になっている質問も公開してしまいます。
本来、公開日が未来であれば、その日付になったときに公開されるようになるべきです。
urls.py
したがって、urls.pyのIndexクラスにおける get_queryset() は以下のように修正しておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def get_queryset(self, latest=5): """ 最新latest個の質問を返す :param latest: :return: """ # 公開日の大きいものでソートして取得 # filter()で現在日時より小さいものを取得する questions = db.session.query(Question).\ filter(Question.pub_date <= datetime.now()).order_by(Question.pub_date.desc()).all() db.session.close() return questions[:latest] |
詳細ページの修正
今の所、とてもうまくアプリケーションは動いています。
しかし、まだ考慮すべき点が残っています。
先ほど未来の質問は表示しないようにコードを修正しましたが、実はURLを知っていれば未来の質問にたどり着けてしまいます。
もし、未来の質問詳細ページにアクセスしようとする人がいたら、質問一覧のトップページにリダイレクトさせるようにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @api.route('/detail/{q_id}') class Detail: async def on_get(self, req, resp, q_id): db.session.close() question = db.session.query(Question).filter(Question.id == q_id).first() choices = db.session.query(Choice).filter(Choice.question == q_id).all() db.session.close() # New! 公開日前の日付なら404リダイレクト if question.pub_date > datetime.now(): resp.content = self.template('404.html') resp.content = api.template('/detail.html', question=question, choices=choices) |
第6回へつづく!
今回は、テストとバグ取りについてお話ししました。
Responderには、Djangoと違って独自のテスト機能はありません。
最初に言ったようにPytestを用いたテストをしているため、Responderにおけるテストは工夫が必要になってきます。
本記事では、簡単なテストケースの紹介しかできませんでした。
ですが、本来、Djangoのチュートリアルではビューのテストも紹介しています。
ResponderとPytestでのビューテストは、かなり複雑になってしまうため今回は飛ばしました。
今後、Responderのアップデートで、テストについて充実していくかもしれませんね。
第6回の記事はこちら
【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン