• トップ
  • ブログ一覧
  • 【第5回】Responderを使ってDjangoチュートリアルをやってみた【自動テスト導入編】
  • 【第5回】Responderを使ってDjangoチュートリアルをやってみた【自動テスト導入編】

    広告メディア事業部広告メディア事業部
    2019.09.27

    IT技術

    第5回~Responderを使ってDjangoチュートリアルをやってみた~

    前回の記事の「【第4回】Responderを使ってDjangoチュートリアルをやってみた【公開ビュー作成編】」の続きです。

    今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。

    Django のチュートリアル「はじめての Django アプリ作成」を、Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。

    【はじめての Django アプリ作成】
    https://docs.djangoproject.com/ja/2.2/intro/

    第1回はこちら

    featureImg2019.08.20【第1回】Responderを使ってDjangoチュートリアルをやってみた【プロジェクト作成編】第1回~Responderを使ってDjangoチュートリアルをやってみた~初期セットアップが、まだお済みでない方は前回...

    自動テストとは?

    この記事は、はじめての 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をインスタンス化しておきます。

    その下に実際にテストする内容を書きます。

    1. クラス名はclass TestXXX
    2. メソッド名は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 =====================

    このようにテスト結果が返ってきます。

    返ってきた内容は以下のようになります。

    1. ディレクトリ内のテストファイルを探す
    2. テストファイルを実行し、成功したか test_sample.py F
    3. 失敗したテストと、原因となる処理

    バグを修正する

    問題の場所が特定でき、バグへの対処法も分かっています。

    未来の日付のときは、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回の記事はこちら

    featureImg2019.08.20【第6回】Responderを使ってDjangoチュートリアルをやってみた【静的ファイル管理編】第6回~Responderを使ってDjangoチュートリアルをやってみた~前回の記事の「【第5回】Responderを...

    【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた

    featureImg2019.10.25【まとめ編】Responderを使ってDjangoチュートリアルをやってみたResponderを使ってDjangoチュートリアルをやってみた~まとめ~ライトコード社長も今、イチオシのWEBフレー...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事