A.blog

a-pompom

Djangoを学んでみる-HelloWorld(テストコード)

投稿日: 2022年1月28日 更新日: 2022年1月28日

カテゴリ: Python Django Django入門

概要

DjangoでつくったHello Worldアプリケーションのテストコードを書いてみます。

ゴール

Djangoアプリケーションでどのようにテストコードを書くのか、大まかな指針を理解することを目指します。

目次

用語整理

全体の方針

Webアプリケーションフレームワークでテストコードを書くには、何を検証するのか・どこまで動作を確かめるのか・具体的なコードでどのように書くのか、など考えることがたくさんあります。このまま立ち向かうと何から手をつけてよいか定まらず、行き詰まってしまいます。

ですので、まずはテストコードへ取り掛かる上で何を考える必要があるのか明らかにしておきます。検討すべきは、

といったところでしょうか。この場ですべてを掘り下げるとHello Worldどころの話ではなくなってしまうので、ざっくり全体像を眺める程度にとどめておきます。個々の考え方は少しずつテストコードを書きながら、なぜそのように書くのか明記しながら深めていくことにしましょう。

ツール

詳細は次節へ譲りますが、基本方針は「なるべくシンプルにテストコードを書けるものが欲しい」としておきます。

テスト範囲

ここでの範囲は、例えばHTTPレスポンスに含まれるHTML文字列がすべて期待通りであるとするか・あるいは、HTTPレスポンスを組み立てる元となるものが期待通りであるとするか、を意味します。つまり、どこまで厳密に検証するかということですね。

後々アプリケーションをつくる度に考えていきますが、大方針は「頑張り過ぎないテストコード」を心掛けていきます。すべてを検証しようと思うと、メンテナンスしきれない巨大なテストコードが出来上がってしまうので、最低限担保したいゴールを都度定めることを目指します。

テストの書き方

こちらもテストコードを書くときに詳しく見ていきますが、基本の考え方は「コードもテストコードもシンプルに」を心がけていきます。あまりにも面倒な書き方になってきた・今テストコードで何を検証しているのか一言で表現できない、と思えてきたら実装やテストコードをもう少しシンプルにできないか、考えるようにしましょう。


こうして見てみると、色々と考えることが多くて大変そうです。ですが、テストコードを書いていけばDjangoの理解も深まってくるはずなので、一歩ずつ立ち向かっていきましょう。

ということで、以降では最初にどのテストフレームワークを選ぶか検討し、その後Hello Worldアプリケーションのテストコードの書き方をたどっていくことにします。

テストフレームワークの選び方

Djangoでテストコードを書くとき、真っ先に思い浮かぶのは、公式で提供されているツールでしょう。

ですが、今回はpytest・pytest-djangoというフレームワーク・ツールを組み合わせてテストコードをつくり上げていきます。

なぜ公式で用意されているものを使わないのか

実装・テストコードも含め、Django公式から提供されているもので完結させた方がすっきりするように思えます。なぜわざわざ他のツールを導入するのでしょうか。

これは、先ほど名前が挙がったpytestというテストフレームワークがとてもシンプルで使いやすい、ということが大きな理由です。詳細なメリットなどは公式を見ていただいた方が分かりやすいかと思います。

pytest-djangoとは

pytest-djangoにも触れておきます。pytest-djangoはpytestのプラグインの1つで、Djangoアプリケーションのテストコードをpytestで書くとき、さまざまな補助機能を提供してくれます。最も大きなメリットはデータベース操作をよしなにやってくれることですが、実際に踏み込むのはDjangoでデータベースを扱うようになってからにしておきます。

ひとまず今は、pytestとpytest-djangoがあればDjangoのテストコードがシンプルに書ける、ということを頭の隅に置いておきましょう。

pytest・pytest-djangoのインストール

Djangoと同様、pipでインストールすることができます。

    $ pip install pytest
$ pip install pytest-django
    

Hello Worldのテスト

ようやく手を動かすところへたどり着きました。Hello Worldという単純な処理ではありますが、アプリケーションを動かすだけでは意識しなかった機能もいくつか触ることになるので、ゆっくりと見ていきましょう。

ディレクトリ構成

最初にディレクトリ構成を決めておきます。Djangoではappをつくると、tests.pyを自動生成してくれます。しかし、アプリケーションのコードとテストコードは分かれていた方がシンプルで見やすいです。よって、以降は独立したtestsディレクトリへテストコードを書いていくことにします。より具体的には、アプリケーションのコードはsrc・テストコードはtestsディレクトリと分離させます。

参考

    $ tree
# srcディレクトリへアプリケーションを・testsディレクトリへテストコードを配置
.
├── src
│ ├── __init__.py
│ ├── config
│ ├── hello_world
 └── manage.py
└── tests
    

viewのテスト

DjangoのHello Worldアプリケーションでは、プロジェクト・appを含むさまざまなものがつくられました。ここで、Hello World appの機能だけに注目すると、実装したのはview・URLconfだけでした。

新しくつくったview・URLconfが期待通りに動作していれば、Hello World appを検証できたと言えそうです。

どう検証するか

view・URLconfが想定した通りに動いてくれるか確かめようと思ったとき、大きな問題に直面します。

アプリケーションを動かしたときは、ブラウザでよしなに何かが表示されたらOK、ぐらいの考えでも問題ありませんでした。しかし、テストコードではどうやって正しさを保証するか、考えなければなりません。これはアプリケーションを実装したときとは別の視点が必要となります。

こう書くと何やら大変そうに思えますが、ありがたいことにDjangoでは、テストコードのためのClientというモジュールを提供してくれています。

ClientモジュールはHTTPクライアントとして振る舞ってくれるので、指定したURLへのリクエストから生成されたHTTPレスポンスオブジェクトを検証していけば、view・URLconfの動作を確かめることができます。もう少し詳しく書くと、response = client.get('/someURL')のような記述から得られるresponseオブジェクトから、URLconfが対応づけたviewが生成するレスポンスが見られます。

テストコード

どうやって書くかイメージが固まってきたかと思うので、そろそろテストコードを見てましょう。

    # tests/views_test.py
# テスト用のHTTPクライアント
from django.test.client import Client


class TestHelloWorldView:
    """ Hello Worldのviewが呼び出せるか """

    # ステータスコードは正常か
    def test_status_200(self):
        # GIVEN
        client = Client()
        # WHEN
        # URLconfでHello Worldのviewと対応付けたURL(/)へGETリクエストを送信
        actual = client.get('/')
        # THEN
        # HTTPレスポンスのステータスコードを検証
        # ステータスコードが404や500でなければ、URLで対応するviewが存在していると言える
        assert actual.status_code == 200
    

このテストコードで確かめたいことを改めて言葉にすると、特定のURL(/)へリクエストを送信すると、URLconfをもとに対応するview(hello_world)が呼び出され、HTTPレスポンスが返却されることとなります。完璧とは言えませんが、ある程度担保しておきたい動作を「頑張り過ぎない範囲で」カバーできているのではないかと思います。

Client

ClientクラスでGETリクエストを送信するときの書式を見ておきます。

参考

書式: get(path)

また、引数のpathはURLのドメイン名以降(ex: /blog/article)を指定します。

補足: HTTPレスポンスのボディは検証すべきか

viewの動作を保証するのであれば、HTTPレスポンスのボディまで確かめた方がよさそうです。どうしてステータスコードにとどめているのでしょうか。

これは、ボディにまで踏み込まないことでテストコード・テスト方針をシンプルに保つためです。例えば、複雑なアプリケーションであれば、ボディにはHTMLやスタイル・果てはJavaScriptまで含まれます。これらが混ざった文字列はDjango以外からも影響を受けるので、テストが崩れやすくなってしまいます。

少し話が難しくなってきたので整理しましょう。

HTTPレスポンスのボディはDjango以外も関わってつくられます。Djangoの世界に限定してテストコードを書きたいので、HTTPレスポンスのボディは対象外とします。影響をDjangoに関わるものに閉じてしまえば、安全かつシンプルにテストコードを書くことができます。

テストを動かす準備

さて、あとは書いたコードが期待通りに動いてくれればよさそうです。しかし、このままではDjangoアプリケーションを検証することができません。テストコードでDjangoを動かすには、少しだけDjangoの裏側へ踏み込まなければなりません。具体的には、テストコードへ前処理として、Djangoの前準備を実行させます。

普段アプリケーションを書く上ではあまり意識することのないところですが、この機会に覗いてみましょう。

    # tests/conftest.py
import django
import os
from pathlib import Path
import sys


def pytest_sessionstart(session):
    """ Djangoテストの前処理 """

    # 各種モジュールをimportできるようsrcディレクトリをimportパスへ追加
    src_directory = Path(__file__).resolve().parent.parent / 'src'
    sys.path.append(str(src_directory))

    # 利用する設定ファイル
    os.environ['DJANGO_SETTINGS_MODULE'] = 'config.settings'
    # Djangoの各種モジュールを参照するための準備を整える
    django.setup()
    

上のコードは、pytestでテストを実行するときに前もって実行される処理です。

参考

大きく分けて2つの処理が書かれているので、それぞれもう少し詳しく見てみましょう。

sys.path

Pythonはモジュールをimportするときsys.pathという、文字列のリストへ記述されたパスを探索します。ここで、普段Djangoアプリケーションを動かすときは、manage.pyをPythonインタープリタで実行することで、プロジェクトディレクトリがsys.pathへ追加されます。しかし、テストコードはpytestから実行されるので、何もしなければプロジェクトディレクトリはsys.pathへ登録されません。

これではテスト対象を見つけられずに困ってしまうので、明示的にsys.pathへ追加しています。

django.setup()

Djangoでアプリケーションを書くときには見かけなかった関数が登場しました。django.setup()は、Djangoで開発サーバを起動するときに裏側で実行されていた以下の処理を担ってくれます。

参考

また、上で書かれた環境変数定義は、設定ファイルを読み込むときに参照することで、利用すべき設定ファイルを見つけることができます。

つまり、ここで書かれた処理は、Djangoが基準とする設定ファイルの場所を伝え、各種appを読み込むよう依頼しているのです。


これらを前処理として実行しておくことで、普段Djangoアプリケーションが開発サーバとともに動いているような環境でテストコードを動かすことができます。

テスト実行結果

    $ pytest ./views_test.py 
================================================================================== test session starts ===================================================================================
# 中略...
collected 2 items                                                                                                                                                                        

views_test.py::TestHelloWorldView::test_status_200 PASSED                                                                                                                          [ 50%]
views_test.py::TestHelloWorldView::test_response PASSED                                                                                                                            [100%]

=================================================================================== 2 passed in 0.07s ====================================================================================
$ pytest ./views_test.py
================================================================================== test session starts ===================================================================================
# 中略...
collected 1 item                                                                                                                                                                         

views_test.py::TestHelloWorldView::test_status_200 PASSED                                                                                                                          [100%]

=================================================================================== 1 passed in 0.04s ====================================================================================
    

テストコードを実行することで、めでたくHello Worldアプリケーションの動作を確認することができました。

まとめ

DjangoでつくったHello Worldアプリケーションを対象にテストコードを書いてきました。アプリケーションをつくるのとはまた違った知識が必要でしたが、ここで身につけたことは実装・テストコード相互に活かせるはずです。今後も色々なアプリケーションのテストコードを書きながら理解を深めていきましょう。


記事はGitHubでも公開しています。間違い・よりよい書き方などございましたらIssueやPRを頂けるとうれしいです。

Author:

a-pompom:

GitHub, Bluesky