Pythonのテストフレームワークnose2を使ってみよう
書きかけ...
nose2 について
nose2はPythonで人気のテストフレームワークで、プロジェクトの単体テストを検出して実行することができます。もしあなたがPythonの標準ライブラリであるunittestに慣れ親しんでいて、他のPythonのテスト自動化フレームワークよりもunittestを好むのであれば、Nose2 は使用することを検討してみてください。
nose2 には次のような特徴があります。
Pythonの標準ライブラリにある unittest でのテストスイートもサポート
pytestのテストイディオムをサポート
インストールとメンテナンスが容易
柔軟で豊富なカスタマイズ
簡単に言うと、Nose2はunittestモジュールの拡張版です。nose2は unittest をベースにしており、豊富なプラグインエコシステムによってフレームワークにより高い価値を与えています。
nose2 の前身のNoseは今でも開発者やテスト関係者の一定の割合で使用されています。
インストール
code: bash
$ pip install nose2
こにより、nose2 がインストールされ、nose2 および、Python の -m nose2 でテストフレームワーク nose2 を実行できるようになります。
code: bash
$ nose2
code: bash
$ python -m nose2
--help オプションだけを与えると、簡単なヘルプメッセージが表示されます。
code: bash
$ nose2 --help
positional arguments:
testNames
optional arguments:
-s START_DIR, --start-dir START_DIR
Directory to start discovery ('.' default)
(以下略)
Nose2パッケージをインポートするには、実装でimport nose2を使用します。パッケージの特定のモジュールを使用する場合は、次のようにインポートします。
code: python
from nose2.<package_name> import <module_name>
Nose2におけるテスト発見
nose2で実行されるモジュール(またはファイル)やディレクトリ、メソッド名等には命名規則があります。
__init__.py を含む
小文字のtestを含むディレクトリ
ディレクトリ名は、srcもしくはlib
ファイル名はtestで始まっていること
テストメソッドを内包するテストクラスは Test で始まるべきです。
Nose2 には、テストモジュールの自動検出を実装したプラグインがあります。このプラグインは、パッケージやディレクトリの中で、test で始まる名前のモジュール(またはテストファイル)を探します。そして、発見されたすべてのモジュールに対してloadTestsFromModule() フックを実行し、他のプラグインが実際のテストをロードできるようにします。
nose2の使用方法
nose2はunittestをベースにしているので、Python Standard Libraryのunittestから始めて、その上にnose2を使って付加価値をつけることができます。
nose2はpythonファイルの中でtestで始まる名前のテストを探し、発見したすべてのテスト関数を実行します。
ここでは、典型的なunittestスタイルで書かれたシンプルなテストの例を紹介します。
code: test_01_sample.py
import unittest
class TestStrings(unittest.TestCase):
def test_upper(self):
self.assertEqual("spam".upper(), "SPAM")
このテストを実行するためには、次のようにコマンドを実行します。
code: bash
$ nose2 -v
test_upper (test_01_sample.TestStrings) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
しかし、nose2は単体のunittestよりも多くのテスト構成をサポートし、多くのツールを提供しています。
パラメタライズテスト
code: test_02_fancy.py
from nose2.tools import params
@params("Sir Bedevere", "Miss Islington", "Duck")
def test_is_knight(value):
assert value.startswith('Sir')
これを実行すると、次のような結果になります。
code: bash
$ nose2 -v --pretty-assert
test_02_fancy.test_is_knight:1
'Sir Bedevere' ... ok
test_02_fancy.test_is_knight:2
'Miss Islington' ... FAIL
test_02_fancy.test_is_knight:3
'Duck' ... FAIL
test_upper (test_01_sample.TestStrings) ... ok
======================================================================
FAIL: test_02_fancy.test_is_knight:2
'Miss Islington'
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/04_Nose2/test_02_fancy.py", line 5, in test_is_knight
assert value.startswith('Sir')
AssertionError
>> assert value.startswith('Sir')
values:
value = 'Miss Islington'
value.startswith = <built-in method startswith of str object at 0x110187d70>
======================================================================
FAIL: test_02_fancy.test_is_knight:3
'Duck'
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/04_Nose2/test_02_fancy.py", line 5, in test_is_knight
assert value.startswith('Sir')
AssertionError
>> assert value.startswith('Sir')
values:
value = 'Duck'
value.startswith = <built-in method startswith of str object at 0x110187d30>
----------------------------------------------------------------------
Ran 4 tests in 0.002s
FAILED (failures=2)
テストのスキップ
次のテストファイルは unittest で使用していたものですが、これを nose2 でも実行することができます。
code: test_03_unittest.py
import unittest
import requests
class DemoTest(unittest.TestCase):
status = 200
def setUp(self):
@unittest.skip('無条件にスキップ')
def test_request1(self):
r1 = requests.get(self.url)
@unittest.skipIf(status > 200, 'status が 200より大きい時はスキップ')
def test_request2(self):
# アサーションの結果が真であれば続行し、
# そうでなければテストをスキップします。
r2 = requests.get(self.url)
status2 = r2.status_code
self.assertTrue(status2 > self.status)
@unittest.skipUnless(status == 404, 'status が 404 でなければスキップ')
def test_request3(self):
# 結果が真でない限り、このテストをスキップします。
r3 = requests.get(self.url)
status3 = r3.status_code
self.assertTrue(status3 > self.status)
@unittest.expectedFailure
def test_request4(self):
# テストケースは "expected failed"(予想される失敗)と表示されます。
# テストが実行された場合、テスト結果は失敗ではないと判断します。
r4 = requests.get(self.url+'/posts%2F4331797390207884')
status4 = r4.status_code
self.assertTrue(status4 ==self.status)
def tearDown(self):
pass
if __name__ == '__main__':
unittest.main()
code: bash
% nose2 -v test_03_unittest
test_request1 (test_03_unittest.DemoTest) ... skipped 無条件にスキップ
test_request2 (test_03_unittest.DemoTest) ... FAIL
test_request3 (test_03_unittest.DemoTest) ... skipped status が 404 でなければスキップ
test_request4 (test_03_unittest.DemoTest) ... expected failure
======================================================================
FAIL: test_request2 (test_03_unittest.DemoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/goichiiisaka/Projects/Python.Osaka/Tutorial.Testing/04_Nose2/test_03_unittest.py", line 19, in test_request2
self.assertTrue(status2 > self.status)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 4 tests in 2.097s
FAILED (failures=1, skipped=2, expected failures=1)
nose2のフィクスチャ
nose2 はクラス、モジュール、テスト(またはメソッド)レベルでフィクスチャをサポートしており、これは PyTest のような他のポピュラーなフレームワークでも同じです。Nose2のフィクスチャの使い方を示すために、先ほどの例でsetUp()とtearDown()のフィクスチャを追加してみます。
code: test_04_fixture.py
import unittest
from mymath import add, multiply, square, cube
class MyMathFixtureTest(unittest.TestCase):
test_data = {
'Add': {'a':10, 'b': 20, 'expect': 30},
'Multiply': {'a':5, 'b': 6, 'expect': 30},
'Square': {'a':5, 'b': None, 'expect': 25},
}
@classmethod
def setUpClass(cls):
print(f'\ncalled once before any tests in {cls.__name__}')
@classmethod
def tearDownClass(cls):
print(f'\ncalled once after all tests in {cls.__name__}')
def setUp(self):
self.a = 0
self.b = 0
name = self.shortDescription()
print(f'\nSetup for {name}: {self.a}, {self.b}')
def tearDown(self):
print(f'\nEnd of test {self.shortDescription()}')
def testAdd(self):
"""Add"""
result = add(self.a, self.b)
def testMultiply(self):
"""Multiply"""
result = multiply(self.a, self.b)
def testSquare(self):
"""Square"""
result = square(self.a)
if __name__ == '__main__':
unittest.main()
code: bash
% nose2 -v test_04_fixture
called once before any tests in MyMathFixtureTest
testAdd (test_04_fixture.MyMathFixtureTest)
Add ...
Setup for Add: 10, 20
End of test Add
ok
testMultiply (test_04_fixture.MyMathFixtureTest)
Multiply ...
Setup for Multiply: 5, 6
End of test Multiply
ok
testSquare (test_04_fixture.MyMathFixtureTest)
Square ...
Setup for Square: 5, None
End of test Square
ok
called once after all tests in MyMathFixtureTest
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
テストフィクスチャのレイヤー化
Nose2のコンセプトであるレイヤーについて見ていきます。従来のフィクスチャに比べてレイヤを使うことの主な利点は、フィクスチャの構成に柔軟性があることです。目的は、Zopeのtestrunnerで使われているレイヤーとの互換性を持つことです。
レイヤーを使うことで実現できることは以下の通りです。
パッケージ内の全てのテストケースでレイヤーを共有することによる、パッケージレベルのフィクスチャの実装。
利用可能なレベル(test, class, module)よりもはるかに深いフィクスチャツリーの作成。
異なるモジュールのテスト間でフィクスチャを共有することで、テストを複数回実行する必要がない。
ここでは、少なくともsetUpメソッドを実装したクラスの簡単なデモをご紹介します。
code: python
class Layer(object):
@classmethod
def setUp(cls):
# .........................
tearDown()、testSetUp()、testTearDown() の各メソッドはクラスメソッドとして実装することもできます。ここでは、レイヤーで使用できるメソッドについて簡単に説明します。
setUp(cls) - 特定のレイヤーに属するテストが実行される前に呼び出されます。
testSetUp(cls [, test]) - そのレイヤー(およびそのサブレイヤー)に属する各テストの実行前に呼び出されます。このメソッドは、テストケースのインスタンスがメソッドに渡される引数を受け入れることができます。
tearDown(cls) - レイヤーに属するテストが実行された後に呼び出されます。レイヤーが結合されたsetUpメソッドを持っていない場合や、何らかの例外でsetUpが実行できなかった場合には呼び出されません。
testTearDown(cls [, test]) - レイヤー(およびそのサブレイヤー)に属する各テストが実行された後に呼び出されます。これは、レイヤーが setUp (または testSetUp) テスト メソッドを定義し、そのメソッドが問題なく実行された場合にのみ呼び出されます。
テストケースにレイヤーを割り当てるには、以下のようにテストケースのレイヤープロパティを設定する必要があります。
code: python
class Test(unittest.TestCase):
layer = Layer
実装でレイヤーを使用する前に、 nose2.cfg (または unittest.cfg) で、プラグイン nose2.plugins.layers をロードする必要があります。
code: nose.cfg
plugins = nose2.plugins.layers
always-on = True
descriptions = True
always-on = True
colors = True
テストディスカバリーの開始場所をインポートディレクトリの変更
ディスカバリーの開始場所を変更したり、プロジェクトのトップレベルのインポート可能なディレクトリを変更したりするには、-sおよび-tオプションを使用します。
-s START_DIR, --start-dir START_DIR
ディスカバリーを開始するディレクトリ。デフォルトでは、現在の作業ディレクトリになります。このディレクトリが nose2 がテストを探し始める場所です。
-t TOP_LEVEL_DIRECTORY, --top-level-directory TOP_LEVEL_DIRECTORY, --project-directory TOP_LEVEL_DIRECTORY
プロジェクトの最上位ディレクトリ。デフォルトは開始ディレクトリです。これは、インポート可能なモジュールとパッケージを含むディレクトリで、テストの発見が始まる前に、常に sys.path の前に付けられます。
実行するテストの指定
個々のテストモジュール、クラス、またはテストを実行するために、コマンドラインで nose2 にテスト名を渡します。
テスト名は、pythonオブジェクト部分と、ジェネレータやパラメータ化されたテストの場合は、引数部分で構成されます。pythonオブジェクト部分は、pkg1.tests.test_things.SomeTests.test_ok のように、ドット構文で指定された名前です。引数部分は python オブジェクト部分とコロン (:) で区切られ、選択する生成テストのインデックスを 1 から順に指定します。 たとえば、pkg1.test.test_things.test_params_func:1 は、パラメータ化された test_params_func から生成された最初のテストを選択します。
プラグインは、他のテスト選択手段を提供することもできます。
code: bash
% nose2 -v test_01_sample
test_upper (test_01_sample.TestStrings) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
python setup.py testによるテストの実行
nose2はdistribute/setuptoolsの setup.py test規格をサポートしていて、python setup.py tesst でテストを実行することができます。 nose2を使ってパッケージのテストを実行するには、setup.py に以下を追加します。
code: 抜粋 setup.py
setup(...
test_suite='nose2.collector.collector',
...
) # ... は他のなにかという意味で使用しています。
この方法でテストを実行する場合、2つの注意点があります。
1つ目は、setuptoolsのtestコマンドが制限されているため、 nose2は実際にテスト実行プロセスを完全に引き継ぐ「テストスイート」を返し、テスト結果とそれを呼び出すテストランナーをバイパスします。これは、いくつかのパッケージと互換性がないかもしれません。
2つ目は、テストコマンドのコマンドライン引数が nose2 の引数と適切に一致しない可能性があるため、コレクターが起動した nose2 インスタンスはコマンドライン引数を受け付けません。つまり、すべてのテストが常に実行され、この方法でテストを実行する際には、コマンドラインでプラグインを設定することができません。回避策として、testコマンドで実行している場合、nose2 は unittest.cfg と nose2.cfg に加えて、setup.cfg があればそこから設定を読み込みます。これにより、setuptools test コマンドに固有の設定をsetup.cfg に記述することができます。例えば、コマンドラインでアクティベートしなければならないプラグインをアクティベートすることができます。
nose2 と nose の違いについて
nose2 は nose からスピンアウトしたプロジェクトです。nose が前身ですが、いくつかの違いがあります。
何が違うのか
Pythonのバージョン
nose は Python 2.4 以上をサポートしていますが、 nose2 は Python チームが現在サポートしている Python バージョンのみをサポートしています。つまり、Python3 だけをサポートしています。
テストの発見と読み込み
noseはテストモジュールを遅延的にロードします。つまり、最初にロードされたモジュールのテストは、2番目のモジュールがインポートされる前に実行されます。 nose2はすべてのテストを最初にロードしてからテストの実行を開始します。これにはいくつかの重要な意味があります。
nose2は __import__() でテストモジュールをインポートすることができます。
第二に、nose2はnoseのようなテストプロジェクトのレイアウトをすべてサポートしているわけではありません。
具体的には、以下のようなプロジェクトは nose2 で正しくテストをロードできません。
code: bash
.
`-- tests
|-- more_tests
| `-- test.py
`-- test.py
noseのローダーにとって、これら2つのテストモジュールは異なるモジュールのように見えます。しかし、 nose2 のローダーからは同じに見えてしまい、正しくロードされません。
テストフィクスチャ
nose2は、unittest2と同じレベルのフィクスチャしかサポートしていません。つまり、クラスレベルのフィクスチャとモジュールレベルのフィクスチャはサポートしていますが、パッケージレベルのフィクスチャはサポートしていません。また、 nose2 は nose と異なり、コマンドラインで指定されたテストを順番に並べて、同じフィクスチャを持つテストをまとめようとはしません。
パラメタライズドテストとジェネレータテスト
コンフィギュレーション
noseは、プラグインがすべての設定パラメータをコマンドラインオプションとして利用できることを期待しています。 nose2は、ほとんどすべての設定を設定ファイルで行うことを期待しています。プラグインは通常、1つのコマンドラインオプションのみを持つべきです: プラグインを起動するオプションです。その他の設定パラメータは、設定ファイルから読み込む必要があります。これにより、より再現性のあるテスト実行が可能になり、コマンドラインオプションのセットを人間が読める程度に小さくすることができます。
わかりやすくまとめると、直ぐに使い始められるという点では、nose も nose2 も同じですが、nose で複雑なテストスイートを実行使用とすると、都度コマンドラインでパラメタを指示する必要があります。nose2では、それらを設定ファイルに記述することでコマンドラインがシンプルになります。
プラグインのロード
noseはsetuptoolsのエントリーポイントを使用してプラグインの検索とロードを行いますが、 nose2はそうではありません。代わりに、notes2はすべてのプラグインが設定ファイルに記載されていることを要求します。これにより、どこかにインストールされているという理由だけでテストシステムにプラグインがロードされることはなく、テスト対象のプロジェクトの一部であるプラグインを簡単に組み込むことができます。詳しくは Configuring nose2 を参照してください。 python setup.py testの限定的なサポート
nose2はsetuptoolsの python setup.py test コマンドをサポートしていますが、その方法はnoseとは全く異なります。setuptoolsのtestコマンドがカスタムテストランナーで設定できないことによる、noseの内部の複雑さを避けるために、この方法で実行すると、nose2は基本的にテスト実行プロセスをハイジャックします。nose2.collector.collector() が返す「テストスイート」は、実際にはテストケースの中に隠されたテストランナーです。通常通りにテストをロードして実行し、独自のテストランナーとテスト結果を設定し、sys.exit()を自ら呼び出します。これは、一部のプロジェクトとは互換性がないかもしれません。
プラグインAPI
nose2では、unittest2のpluginsブランチでMichael Foord氏が行った作業に基づいて、新しいプラグインAPIを実装しています。このAPIは、特にプラグイン同士のやりとりを可能にする点で、noseのAPIよりも大幅に優れています。しかし、 nose の API とはa明らかに異なっているため、 nose のプラグインを nose2 でサポートすることは実用的ではありません。詳しくは Writing Plugins を参照してください。 不足しているプラグイン
nose2には、noseでよく使われているプラグインが含まれていない。noseの組み込みプラグインの中には、内部構造の違いにより、nose2に移植できないものがあります。以下を参照してください。nose2 に組み込まれているプラグインについては、Plugins for nose2 を参照してください。 内部構造
nose2はTestCasesをラップせず、テスト結果クラスを結果プロキシでラップしていません。これは、TestProgram.__init__() にテストローダーとランナーを指定する引数を許可するのではなく、無条件に行います。詳細はInternals を参照してください。 ライセンス
noseはLGPLでしたが、 nose2はBSDライセンスです。この変更は、大多数の nose のコントリビューターからの要望によるものです。
何が同じなのか
哲学
nose2はnoseと同じ目標を持っています:テストをより良く、より理解しやすくするためにunittestを拡張することです。開発者に柔軟性、パワー、透明性を与え、一般的なテストシナリオには余分な作業を必要とせず、一般的でないテストシナリオは最小限の騒ぎと魔法でサポートできるようにすることを目的としています。
nose2はunittest2/pluginsではありません
nose2はunittest2 pluginsブランチをベースにしていますが、いくつかの点で異なっています。nose2はunittest.TestCaseを置き換えることができないため、イベントAPIは全く同じではなく、テスト実行やプラグインセットをグローバルに設定することはありません。 nose2はまた、いくつかの一般的なケース(エラー出力に追加情報を追加するなど)をよりよくサポートするために、unittest2のプラグインとは完全に異なるレポートAPIを持っています。つまり、unittest2とは異なり、nose2にはデフォルトで有効なプラグインの実質的なセットが含まれているのです。