読者です 読者をやめる 読者になる 読者になる

cloverrose's blog

Python, Machine learning, Emacs, CI/CD, Webアプリなど

Django テストランナーのカスタマイズ

やりたいこと

python manage.py test を実行すると、デフォルトではINSTALLED_APPSに登録されたアプリのmodels.pyとtests.py内のdocstringからテストが作られるが、tests.py以外のスクリプト内のdocstringもテストしたい!

問題

まずはDjangoのテストフレームワークから外れて、一般的なPythonのdoctestを行おうとした。

if __name__ == "__main__":
    import doctest
    doctest.testmod()

を各スクリプトファイルの末尾に追加し、python hoge.py を実行してテストを行おうとした。

しかし、python manage.py shell で起動したシェルでしか実行できないコードがある場合、これではうまくいかないことがわかった。

調査

Djangoのテストフレームワークを拡張して好きなスクリプトファイルをテストするために、Djangoがどのようにテストを実行しているのか(特にtests.pyはどのように参照されているのか)を調べた。

実行の起点はどこ?

Djangoアプリケーションのテスト — Django v1.1 documentationおよびAppendix E: Settingsを見ると、
/usr/lib/python2.7/dist-package/django/test/simple.pyで定義されている

def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None)

を呼び出していることが解る。(v1.1ドキュメントと比べてfailfast=Falseが追加されてた)

tests.pyはどうやって参照されている?

simple.pyで

# The module name for tests outside models.py
TEST_MODULE = 'tests'

というように定義されている。

このTEST_MODULEはget_tests(app_module)内でのみ参照されている。
このget_tests(app_module)はINSTALLED_APPSで定義したアプリフォルダ中のtests.pyをインポートしてモジュールを返している。

よって、get_testsを呼び出す前にsimple.TEST_MODULEを好きな値に書き換えてしまえばよさそうだ。

get_testsはどこから呼ばれてる?

get_tests(app_module)はbuild_suite(app_module)およびbuild_test(label)から呼ばれている。

build_test

build_test(label)の引数であるlabelは
Djangoアプリケーションのテスト — Django v1.1 documentationを見てみると、
python manage.py test myapp -> test_labels=('myapp',)
python manage.py test myapp.SimpleTest -> test_labels=('myapp.SimpleTest',)
python manage.py test myapp.SimpleTest.test_basic_addition -> test_labels=('myapp.SimpleTest.test_basic_addition',)

python manage.py test myapp1 myapp2 -> test_labels=('myapp1', 'myapp2')

というように、実行したいテストを指定した値が代入される。
何も指定しなければ、このメソッドは呼ばれない。

テストを実行するときに今のところすべてを実行しているので、指定した場合の拡張は後回しにすることにする。

build_suite
# Check to see if a separate 'tests' module exists parallel to the
# models module

というコメントがbuild_suite内で見つかる。そしてそのコメントの直後でget_testsが呼ばれている。
よって、ここでget_tests(app_module)を呼ぶ前にTEST_MODULEを書き換えればよさそうだ。

カスタマイズ

テストランナーの設定

デフォルトのテストランナーではなく、自作のテストランナーを起動するようにsettings.pyに以下を追加する。

TEST_RUNNER = 'mysite.run_test.run_tests'

カスタムテストランナーの作成

manage.pyと同じディレクトリにrun_test.pyを次のように作成する。

DjangoTestSuiteRunnerを継承したクラスを定義し、挙動を変えたいメソッドであるbuild_suiteをオーバーライドする。
変更点はsuite.addTest(my_build_suite(app))を2箇所に追加しているだけ。

つぎにmy_build_suite(app)を定義する。これはsimple.pyで定義されているbuild_suite(app)の内、tests.pyからdoctestを生成する部分を切り取って、(test_module = get_tests(app_module) から pass まで)
それをforループ(for n in my_test_modules:)で回したものである。

このmy_test_modulesにtests.py以外でテストしたいdocstringが含まれたファイル名を指定すれば良い。
例ではmy_test_modules=['mytest','awesome']と定義している。
複数のmyapp1/mytest.pyおよびmyapp1/awesome.py, myapp2/mytest.pyというファイルすべてがテスト対象になる。
myapp2/awesome.pyが無いからといってエラーにはならない。

注釈

MyDjangoTestSuiteRunnerクラス内のbuild_suiteメソッド中の

if test_labels:

および

if extra_tests:

条件文は今のところTrueになることはないので、削除してもよさそうだが、互換性のために一応残しておいた。

間違った実装例

my_build_suiteを実装する必要あるの?
MyDjangoTestSuiteRunnerクラス内のbuild_suiteメソッド内で、simple.TEST_MODULEを変更して複数回suite.addTest(build_suite(app))を実行してテストを追加すればいいんじゃないの?

そう思って一応調べてみた。
その実装は以下のようになる。

この場合、models.py内のテストが複数回追加され、その結果予期しない結果が生じる。
例えば、データベースに値を追加して、出力を確認するようなテストの場合、
複数テストが実行されると、データベースには同じ値が複数追加されてしまい、
出力は期待していたものと違う結果となってしまう。

doctestの一般的な注意事項

docstringで日本語使ったテストがエラーになった。
python manage.py shellを起動してから同じ事をしてもエラーにならないのにどうして?と思って調べたら、
docstringにもu""" """というようにuを付けなければいけないということだった。

参考サイト
http://stackoverflow.com/questions/1733414/how-do-i-include-unicode-strings-in-python-doctests