【第5回】Responderを使ってDjangoチュートリアルをやってみた【自動テスト導入編】
IT技術
前回の記事の「【第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"""
2test_question.py
3サンプルテストケース
4"""
5import pytest
6import run as myApp
7
8from datetime import datetime, timedelta
9
10from models import Question
11
12
13@pytest.fixture
14def api():
15 return myApp.api
16
17
18class TestQuestionModel:
19 def test_was_published_recently_with_future_question(self, api):
20 """
21 未来の質問に対してwas_published_recently()はFalseを返すはずである
22 :param api:
23 :return:
24 """
25 # 未来の公開日となる質問を作成
26 time = datetime.now() + timedelta(days=30)
27 feature_question = Question('future_question', pub_date=time)
28
29 # これはFalseとなるはず
30 assert feature_question.was_published_recently() is False
Responderでは、テストはサポートしていないので、pytestと呼ばれるモジュールを使用します。(公式でも推奨)
したがって、import pytest を記述します。
その次に、実際にアプリを実行するファイルをインポート(import run as myApp)します。
ここでは、分かりやすくするため、名前をmyApp としてインポートしています。
1@pytest.fixture
2def api():
3 return myApp.api
そして、ResponderのAPIをインスタンス化しておきます。
その下に実際にテストする内容を書きます。
- クラス名はclass TestXXX
- メソッド名はdef test_XXX(api)
基本的に、クラス名はclass TestXXX 、メソッド名はdef test_XXX(api) としましょう。
ある関数に対するものなら、XXXは関数名の方が望ましいです。
実行
それでは、実行してみます。
1User@User:responder$ pytest
すると、
1============================= test session starts ==============================
2platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
3rootdir: /Users/araki/PycharmProjects/responder
4collected 1 item
5
6test_question.py F [100%]
7
8=================================== FAILURES ===================================
9______ TestQuestionModel.test_was_published_recently_with_future_question ______
10
11self = <test_question.TestQuestionModel object at 0x105d341d0>
12api = <responder.api.API object at 0x104362e90>
13
14 def test_was_published_recently_with_future_question(self, api):
15 """
16 未来の質問に対してwas_published_recently()はFalseを返すはずである
17 :param api:
18 :return:
19 """
20 # 未来の公開日となる質問を作成
21 time = datetime.now() + timedelta(days=30)
22 feature_question = Question('future_question', pub_date=time)
23
24 # これはFalseとなるはず
25> assert feature_question.was_published_recently() is False
26E assert True is False
27E + where True = <bound method Question.was_published_recently of <models.Question object at 0x105d34f10>>()
28E + where <bound method Question.was_published_recently of <models.Question object at 0x105d34f10>> = <models.Question object at 0x105d34f10>.was_published_recently
29
30test_question.py:30: AssertionError
31=============================== warnings summary ===============================
32
33# ~~~ 省略 ~~~ #
34
35-- Docs: https://docs.pytest.org/en/latest/warnings.html
36===================== 1 failed, 3 warnings in 0.56 seconds =====================
このようにテスト結果が返ってきます。
返ってきた内容は以下のようになります。
- ディレクトリ内のテストファイルを探す
- テストファイルを実行し、成功したか test_sample.py F
- 失敗したテストと、原因となる処理
バグを修正する
問題の場所が特定でき、バグへの対処法も分かっています。
未来の日付のときは、False を返すように修正しましょう。
1class Question(Base):
2
3 def was_published_recently(self, days=1):
4 """
5 最近追加された質問に対してTrueを返す関数
6 :return:
7 """
8 now = datetime.now()
9 return now - timedelta(days=days) <= self.pub_date <= now
実行
そうしたら、もう一度テストを実行してみます。
1============================= test session starts ==============================
2platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
3rootdir: /Users/araki/PycharmProjects/responder
4collected 1 item
5
6test_question.py . [100%]
7
8=============================== warnings summary ===============================
9
10# ~~~ 省略 ~~~ #
11
12-- Docs: https://docs.pytest.org/en/latest/warnings.html
13===================== 1 passed, 3 warnings in 0.44 seconds =====================
テストにpassしました!
(Warning はとりあえず今はスルー)
小さなテストコードですが、今後アプリケーションが複雑になったとしても、期待通りの動作をするかどうかを簡単にテストできるようになりました。
より包括的なテスト
ついでに、他のテストケースも書いてみます。
今からやるのは、いわゆる境界値テストというものです。
入力された数値やデータが何かしらの閾(しきい)値によって、出力が異なるような処理を書いた場合、その閾値付近でテストを行います。
例えば、以下のようなテストが良いでしょう。
1 def test_was_published_recently_with_boundary_question(self, api):
2 """
3 == 境界値テスト ==
4 1日1秒前の質問に対してはwas_published_recently()はFalseを返すはずである
5 また,23時間59分59秒以内であればwas_published_recently()はTrueを返すはずである
6 :param api:
7 :return:
8 """
9 # 最近の境界値となる質問を作成
10 time_old = datetime.now() - timedelta(days=1)
11 time_res = datetime.now() - timedelta(hours=23, minutes=59, seconds=59)
12 old_question = Question('old_question', time_old)
13 res_question = Question('resent_question', time_res)
14
15 assert old_question.was_published_recently() is False
16 assert res_question.was_published_recently() is True
1============================= test session starts ==============================
2platform darwin -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
3rootdir: /Users/araki/PycharmProjects/responder
4collected 2 items
5
6test_question.py .. [100%]
7
8=============================== warnings summary ===============================
9
10# ~~~ 省略 ~~~ #
11
12-- Docs: https://docs.pytest.org/en/latest/warnings.html
13===================== 2 passed, 3 warnings in 0.47 seconds =====================
これで、Questionテーブルの公開日に対しては、過去、現在、未来すべてのテストが完成しました。
その他バグ取り
今、作っている投票アプリケーションは、まだ質問を適切に見分けることができません。
pub_dateフィールドが、未来の日付になっている質問も公開してしまいます。
本来、公開日が未来であれば、その日付になったときに公開されるようになるべきです。
urls.py
したがって、urls.pyのIndexクラスにおけるget_queryset() は以下のように修正しておきましょう。
1 def get_queryset(self, latest=5):
2 """
3 最新latest個の質問を返す
4 :param latest:
5 :return:
6 """
7 # 公開日の大きいものでソートして取得
8 # filter()で現在日時より小さいものを取得する
9 questions = db.session.query(Question).\
10 filter(Question.pub_date <= datetime.now()).order_by(Question.pub_date.desc()).all()
11
12 db.session.close()
13
14 return questions[:latest]
詳細ページの修正
今の所、とてもうまくアプリケーションは動いています。
しかし、まだ考慮すべき点が残っています。
先ほど未来の質問は表示しないようにコードを修正しましたが、実はURLを知っていれば未来の質問にたどり着けてしまいます。
もし、未来の質問詳細ページにアクセスしようとする人がいたら、質問一覧のトップページにリダイレクトさせるようにしましょう。
1@api.route('/detail/{q_id}')
2class Detail:
3 async def on_get(self, req, resp, q_id):
4 db.session.close()
5
6 question = db.session.query(Question).filter(Question.id == q_id).first()
7 choices = db.session.query(Choice).filter(Choice.question == q_id).all()
8 db.session.close()
9
10 # New! 公開日前の日付なら404リダイレクト
11 if question.pub_date > datetime.now():
12 resp.content = self.template('404.html')
13
14 resp.content = api.template('/detail.html', question=question, choices=choices)
第6回へつづく!
今回は、テストとバグ取りについてお話ししました。
Responderには、Djangoと違って独自のテスト機能はありません。
最初に言ったようにPytestを用いたテストをしているため、Responderにおけるテストは工夫が必要になってきます。
本記事では、簡単なテストケースの紹介しかできませんでした。
ですが、本来、Djangoのチュートリアルではビューのテストも紹介しています。
ResponderとPytestでのビューテストは、かなり複雑になってしまうため今回は飛ばしました。
今後、Responderのアップデートで、テストについて充実していくかもしれませんね。
第6回の記事はこちら
【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit