前々から、Python, Django 界隈の単体テスト事情をまとめたいと思っていたのですが、こんな素敵なまとめ記事を見つけました。
Python用のユニットテストツールまとめ | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記
この記事では、unittest, unittest2, doctest, py.test, nose のほか、それこそ聞いたことのないようなものまで取り上げてくれているのですが、今回は、自分で使いそうな範囲のテストツールについて少しディープにまとめてみようと思います。
取り上げるのは、以下の 4種類(「Django + unittest」を unittest としてカウントすると 3種類)のテストツールです。
ほかにも、pytest や doctest も取り上げたかったのですが、まだ使ったことがなかったので割愛します(後で書くことになるかもしれません)。
テストツール | 特徴 | カバレッジ |
---|---|---|
unittest | Python標準パッケージに含まれている単体テストライブラリ | × |
Django + unittest | Django の manage.py ユーティリティから、unittest が便利に使うことができる | × |
nose | 多彩なオプションで柔軟なテストができ、カバレッジを取ることもできる。特定のクラスを継承する必要がない | ○ |
django-nose | Django の manage.py ユーティリティから、nose を使えるようにしたもの | ○ |
★ 2015/6/12 追記
pytest については、最近出た「Pythonプロフェッショナルプログラミング 第2版」(第1版ではありません)に、詳しめに書かれていました。pytest 派であれば、是非どうぞ。
- 作者: 株式会社ビープラウド
- 出版社/メーカー: 秀和システム
- 発売日: 2015/05/21
- メディア: Kindle版
- この商品を含むブログを見る
1. unittest
Python 2.7 デフォルトで使えるのは、この unittest です。
全てのテストクラスは、親クラスとして「unittest.TestCase」を継承する必要があり、テストメソッド名にも命名ルールが定められています。
テストケース命名規則
unittest.main() が呼び出されると、そのスクリプトの中で unittest.TestCase を継承したすべてのクラスがテストケースのかたまりとして認識され、そのメソッドのうち名前の先頭が test で始まるメソッドだけがテストケースとして実行されます。
基本的な使い方
### パスを通す $ export PYTHONPATH=$PYTHONPATH:`pwd -P` ### モジュール単位でテスト実行 $ python tests/test_calc.py -v
「-m unittest」を付けた場合は、「PYTHONPATH」を指定しなくても実行可能です。
### モジュール単位でテスト実行 $ python -m unittest -v tests.test_calc ### クラス単位でテスト実行 $ python -m unittest -v tests.test_calc.TestCalculator ### メソッド単位でテスト実行 $ python -m unittest -v tests.test_calc.TestCalculator.test_add_when_arg_is_int ### テストメソッドを自動検索 $ python -m unittest discover -v ### 複数のテストを実行 $ python -m unittest -v tests.test_calc tests.test_random
サンプルコード
. ├── calc.py └── tests ├── __init__.py └── test_calc.py
calc.py
class Calculator(object): def __init__(self, num1): self.num1 = num1 def add(self, num2): if num2 is None: raise Exception("Arg should not be None.") return self.num1 + num2
tests/test_calc.py
import unittest import calc class TestCalculator(unittest.TestCase): def setUp(self): self.calculator = calc.Calculator(2) def test_add_when_arg_is_int(self): expected = 3 actual = self.calculator.add(1) self.assertEqual(expected, actual) def test_add_when_arg_is_float(self): expected = 3.1 actual = self.calculator.add(1.1) self.assertEqual(expected, actual) def test_add_when_arg_is_str(self): self.assertRaises(TypeError, self.calculator.add, '1') def test_add_when_arg_is_none(self): with self.assertRaises(Exception) as em: self.calculator.add(None) e = em.exception self.assertEqual("Arg should not be None.", str(e)) if __name__ == "__main__": unittest.main()
TestSuite を使ってまとめてテストを実行する方法
tests/test_suite.py
# -*- coding: utf-8 -*- import unittest from tests import test_calc, test_random if __name__ == "__main__": loader = unittest.TestLoader() suite = unittest.TestSuite() # テストメソッド単位で追加 suite.addTest(test_calc.TestCalculator('test_add_when_arg_is_int')) # テストクラス単位で追加 suite.addTest(unittest.makeSuite(test_calc.TestCalculator)) suite.addTest(loader.loadTestsFromTestCase(test_calc.TestCalculator)) # テストモジュール単位で追加 suite.addTest(loader.loadTestsFromModule(test_random)) # テストスイートを実行 unittest.TextTestRunner(verbosity=2).run(suite)
使い方
$ python tests/test_suite.py -v
2. Django + unittest
Django には、manage.py のサブコマンドに「test」を指定することで unittest ベースのテストが実行できるユーティリティコマンドが用意されており、また、Django のテストをより簡便にするために「unittest.TestCase」をラップした「django.test.TestCase」も用意されています。
Django のテストランナは、以下の二つの場所からユニットテス トを探します:
models.py ファイル。テストランナはこのモジュールから unittest.TestCase のサブクラスを探します。
アプリケーションディレクトリ、すなわち models.py の入ったディレク トリ下に置かれた tests.py という名前のファイル。上と同様に、テス トランナはこのモジュールから unittest.TestCase のサブクラスを探し ます。
http://django-docs-ja.readthedocs.org/en/latest/topics/testing.html
使い方
テストを実行するには、プロジェクトの manage.py ユーティリティを使います:
$ ./manage.py test特定のアプリケーションに対してテストを実行したければ、コマンドラインにアプ リケーションの名前を追加します。例えば、 INSTALLED_APPS に myproject.polls と myproject.animals というアプリケーションが入って おり、 animals の単体テストを実行したいだけなら、以下のようにします:
$ ./manage.py test animals
http://django-docs-ja.readthedocs.org/en/latest/topics/testing.html#running-tests
### 全テストを実行(ただし、Django 本体のテストも全て実行してしまう) $ python manage.py test -v 2 ### アプリケーション単位でテスト実行 $ python manage.py test polls -v 2 $ python manage.py test polls animals -v 2 ### モジュール単位でテスト実行 (不可??) ### クラス単位でテスト実行 $ python manage.py test polls.PollMethodTests -v 2 ### メソッド単位でテスト実行 $ python manage.py test polls.PollMethodTests.test_was_published_recently_with_future_poll -v 2
サンプルコード
. ├── LICENSE ├── README.md ├── manage.py ├── mysite │ ├── __init__.py │ ├── db.sqlite3 │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── polls ├── __init__.py ├── admin.py ├── models.py ├── templates │ └── polls │ ├── detail.html │ ├── index.html │ └── results.html ├── tests.py ├── urls.py └── views.py
polls/tests.py
https://github.com/akiyoko/django-poll-app/blob/master/polls/tests.py
(参考)
django.test.TestCaseを使うとリクエスト生成などが容易にテストできる
3. nose
nose には様々なオプションが用意されていて、柔軟にテストを実行することができ、カバレッジも取ることができます。
なお、 nose は Python標準パッケージに含まれていないため、pip install が必要です。
事前準備
nose を pip でインストールします。
必要に応じて、(カバレッジを取得するための)coverage も pip install しておきます。
$ sudo pip install nose $ sudo pip install coverage
テストケース命名規則
nose は、単体テスト実行時のカレントディレクトリから、nose のテストケースの命名規則に基づき再帰的にテストケースを探し、実行してくれます。
細かいルールは公式ドキュメントを確認してほしいのですが、簡単に言うと、モジュール名(ディレクトリ名)、ファイル名、関数名、クラス名、メソッド名に "test" という単語が含まれて入ればテストケースとして認識されます。基本的にはケースセンシティブで評価されるようですが、クラス名の場合は "Test" でも認識されました。名前さえ気をつけてテストケースを書けば、実行するテストケースを一覧で逐一指定していくというようなことは必要はありません。
特に、テストクラス名については、「TestXxxx」のようにクラス名の先頭が「Test」で始まらないとテストケースとして見なしてくれないので要注意です(「XxxxTest」だとダメ)。
基本的な使い方
### モジュール単位でテスト実行 $ nosetests -v tests.test_calc $ nosetests -v tests/test_calc.py ### クラス単位でテスト実行 $ nosetests -v tests.test_calc:TestCalculator $ nosetests -v tests/test_calc.py:TestCalculator ### メソッド単位でテスト実行 $ nosetests -v tests.test_calc:TestCalculator.test_add_when_arg_is_int $ nosetests -v tests/test_calc.py:TestCalculator.test_add_when_arg_is_int ### テストメソッドを自動検索 $ nosetests -v ### 複数のテストを実行 $ nosetests -v tests.test_calc tests.test_random $ nosetests -v tests/test_calc.py tests/test_random.py ### エラーになったときに pdb を起動 $ nosetests --pdb
その他の重要なオプションとして、「-s」を付けると、テストコード中の print文の出力もされるようになります。
カバレッジ取得方法
$ nosetests --with-coverage --cover-html --cover-erase ### 条件網羅(C1)カバレッジを取得する場合 $ nosetests --with-coverage --cover-branches --cover-html --cover-erase $ coverage html --include './*' --omit 'tests/*'
オプション | 効果 |
---|---|
--cover-html | HTMLファイルとしてカバレッジレポートを出力 |
--cover-erase | テスト実行前に前回のテスト結果を削除 |
サンプルコード
. ├── calc.py └── tests ├── __init__.py └── test_calc.py
calc.py
(同上)
tests/test_calc.py
import nose from nose.tools import raises, assert_true, assert_equal, assert_raises import calc class TestCalculator(object): def setup(self): self.calculator = calc.Calculator(2) def test_add_when_arg_is_int(self): expected = 3 actual = self.calculator.add(1) assert_equal(expected, actual) def test_add_when_arg_is_float(self): expected = 3.1 actual = self.calculator.add(1.1) assert_equal(expected, actual) @raises(TypeError) def test_add_when_arg_is_str(self): self.calculator.add('1') def test_add_when_arg_is_none(self): with assert_raises(Exception) as em: self.calculator.add(None) e = em.exception assert_equal("Arg should not be None.", str(e))
ちなみに、ok_, eq_ といった assert_true, assert_equal のエイリアスも用意されています(が、名前が気に入らないので個人的には使いません・・)。
なお、
if __name__ == "__main__": import nose nose.main(argv=['nose', '-v'])
というコードを加えておくと、
$ export PYTHONPATH=$PYTHONPATH:`pwd -P` $ python tests/test_calc.py
という形でも、テストコードを実行することができます。
4. django-nose
Django上で「Django + unittest」のような使い方をすることができ、さらに「nose」の柔軟なオプションが使えるようにしたのが、この「django-nose」です。
django-nose は Python標準パッケージに含まれていないので、pip install が必要です(なお、nose も必要なのですが、django-nose インストール時に自動的にインストールされます)。
事前準備
django-nose をインストールします。
必要に応じて、(カバレッジを取得するための)coverage も pip install しておきます。
$ sudo pip install django-nose $ sudo pip install coverage
settings.py に以下を追加します。
INSTALLED_APPS = ( ・ ・ 'django_nose', 'polls', ) TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_APPS = ( 'polls', ) NOSE_ARGS = [ '--with-coverage', '--cover-package=' + ",".join(TEST_APPS), '--cover-branches', '--cover-erase', ]
nosetests のオプションは上記のように「NOSE_ARGS」に書くか、あるいは
$ python manage.py test polls/tests.py --with-coverage --cover-package=polls --cover-branches --cover-erase
のように直接、実行時に指定します。
テストケース命名規則
「Django + unittest」方式のテストケース命名規則と「nose」方式のテストケース命名規則のどちらでも、テストケースとして見なしてくれる(探索してくれる)ようです。
基本的な使い方
### 全テストを実行 $ python manage.py test -v 2 ### アプリケーション単位でテスト実行 $ python manage.py test polls -v 2 ### モジュール単位でテスト実行 $ python manage.py test polls.tests -v 2 $ python manage.py test polls/tests.py -v 2 ### クラス単位でテスト実行 $ python manage.py test polls.tests:PollMethodTests -v 2 $ python manage.py test polls/tests.py:PollMethodTests -v 2 ### メソッド単位でテスト実行 $ python manage.py test polls.tests:PollMethodTests.test_was_published_recently_with_future_poll -v 2 $ python manage.py test polls/tests.py:PollMethodTests.test_was_published_recently_with_future_poll -v 2
カバレッジ取得方法
$ coverage html --include 'polls/*' --omit '*/tests/*'
5. pytest
正直、まだ使ったことがありません。
nose, unittest.py, doctest.py を統合したテスト実行や、様々なオプションで柔軟なテストが実行できるようです。もちろん、カバレッジも取得可能です。
使い方
$ python -m pytest [...] $ py.test test_mod.py # モジュール内のテストを実行 $ py.test somepath # 指定したパスの全てのテストを実行
「使用方法とテスト実行」より