Pylons プロジェクトは、このドキュメントの元となった Tres Seaver によって書かれた Avoiding Temptation: Notes on using unittest effectively に倣ったユニットテスト教義をかなり厳格に守っています。
Note
このドキュメントは、ほとんど排他的に 単体 テストを扱います。 結合テストまたは機能テストに対しては私たちは特別の教義を持っていません。 しかし、下記の tips の多くはその文脈でも再利用できます。
最初の動機に関連したほとんどの問題には、このドキュメントの後で十分に 取り組みます: テストモジュール間のコードの共有を拒絶することは、ほとんど の賢さへの誘惑をなくします。残りの誘惑に対して、その対策は個別のテストを 見て次の質問をすることです:
“don’t repeat yourself” の軸に沿って失敗するテストの修正は、通常は直裁的 (straightforward) です:
このパターンに一致するようにテストを書き直すことには多くの利点があります:
ここで概説されているテストのゴールは、単純さ、疎結合 (あるいは結合がないこと)、 速度です:
開発者によって指定された契約によって AUT が不変であることを確認するため、 開発者はこのようなテストを書きます。この種のテストケースは、例えそれが テストしようとする契約の説明に役立つとしても、 API ドキュメンテーション、 あるいは narrative /「動作に関する理論」ドキュメンテーションの代わりに はなりません。また、それらはもちろんエンドユーザドキュメンテーションを 意図したものでもありません。
doctest はドキュメンテーションとテストの 両方 を提供して、一石二鳥を 達成するように思えます。実際には、 doctest を使用して書かれたテストは、 ほとんど常に貧弱なテストと貧弱なドキュメンテーションにしかなりません。
テスト対象のモジュール (module-under-test; MUT) でインポートに問題があると、 それぞれのテストケースは失敗します: インポートエラーによってテスト実行が 阻害されるべきではありません。テストランナーにもよりますが、 通常のテスト失敗に比べてインポートの問題は一目で識別することがはるかに 難しい場合があります。
例えば以下のようにするのではなく:
1 2 3 4 5 6 7 8 9 10 # test the foo module import unittest from package.foo import FooClass class FooClassTests(unittest.TestCase): def test_bar(self): foo = FooClass('Bar') self.assertEqual(foo.bar(), 'Bar')
このようにしてください:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # test the foo module import unittest class FooClassTests(unittest.TestCase): def _getTargetClass(self): from package.foo import FooClass return FooClass def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_bar(self): foo = self._makeOne('Bar') self.assertEqual(foo.bar(), 'Bar')
単体テストは必要な機能の一部が存在しない環境でも実行できるようにする必要が あります: その場合、1つ以上のテストケースメソッド (TCM) は失敗するでしょう。 あらゆる必要なライブラリーモジュールのインポートをできるだけ遅らせてください。
例えば、 qux モジュールがインポートできない場合、この例ではテストの 失敗がまったく発生しません:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # test the foo module import unittest import qux class FooClassTests(unittest.TestCase): def _getTargetClass(self): from package.foo import FooClass return FooClass def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_bar(self): foo = self._makeOne(qux.Qux('Bar'))
一方、この例は不足しているモジュールを使用する各 TCM の失敗を報告します:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # test the foo module import unittest class FooClassTests(unittest.TestCase): def _getTargetClass(self): from package.foo import FooClass return FooClass def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_bar(self): import qux foo = self._makeOne(qux.Qux('Bar'))
いくつかの場合では、テストケース内で広く使用されるモジュール (しかし MUT ではない!) をインポートすることは合理的なトレードオフかもしれません。 使用法のパターンを明白に理解した後で、おそらくそのようなトレードオフが TCM の寿命の中でやがて生じることがあるでしょう。
少数の大きなテストを書こうとする誘惑を避けてください。理想的には、それぞれの TCM は1つのメソッドあるいは関数の特定の事前条件の組み合わせに対してテストを 行います。例えば、次のテストケースはあまりにも多くをテストしようとしています:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def test_bound_used_container(self): from AccessControl.SecurityManagement import newSecurityManager from AccessControl import Unauthorized newSecurityManager(None, UnderprivilegedUser()) root = self._makeTree() guarded = root._getOb('guarded') ps = guarded._getOb('bound_used_container_ps') self.assertRaises(Unauthorized, ps) ps = guarded._getOb('container_str_ps') self.assertRaises(Unauthorized, ps) ps = guarded._getOb('container_ps') container = ps() self.assertRaises(Unauthorized, container) self.assertRaises(Unauthorized, container.index_html) try: str(container) except Unauthorized: pass else: self.fail("str(container) didn't raise Unauthorized!") ps = guarded._getOb('bound_used_container_ps') ps._proxy_roles = ( 'Manager', ) ps() ps = guarded._getOb('container_str_ps') ps._proxy_roles = ( 'Manager', ) ps()
このテストはいくつかの欠点を持っています。しかし、最も致命的なのは、 あまりにも多くのこと (8つの異なるケース) をテストしているということです。
一般に TCM のプロローグは、 fixture / モックオブジェクト / 静的な値 のセットアップによって特定の事前条件の組み合わせを確立し、次にクラスを インスタンス化するかテスト対象の関数 (function-under-test; FUT) を インポートします。その後 TCM はメソッド / 関数を呼びます。エピローグは 結果をテストします。典型的には、戻り値あるいは1つ以上の fixture / モックオブジェクトの状態のいずれかの検査によって行われます。
テスト対象の関数あるいはメソッドそれぞれに対する事前条件の組み合わせについて 考えることは、契約を明確にすることを助け、より単純な / クリーンな / より速い 実装の動機となるでしょう。
テストの名前は失敗レポートを見る場合の最初にして最も有用な手掛かりと なるでしょう: 何がテストされていたかを考えるために、読者 (一番ありえるのは あなた自身です) にテストモジュールを grep させないでください。
コメントを追加する代わりに、
1 2 3 4 5 class FooClassTests(unittest.TestCase): def test_some_random_blather(self): # test the 'bar' method in the case where 'baz' is not set. ...
テストの目的を示すために TCM 名を使用するようにしてください:
1 2 3 4 class FooClassTests(unittest.TestCase): def test_getBar_wo_baz(self): ...
テストケースクラスの setUp メソッドで不要な仕事をすることは、 TCM 同士の結合性を急激に増加させます。それは悪いこと (Bad Thing) です。 例えば、テスト対象のクラス (class-under-test; CUT) がそのコンストラクタに 引数としてコンテキストを受け取ると仮定してください。 setUp の中で コンテキストをインスタンス化するのではなく:
1 2 3 4 5 6 7 8 9 class FooClassTests(unittest.TestCase): def setUp(self): self.context = DummyContext() # ... def test_bar(self): foo = self._makeOne(self.context)
コンテキストをインスタンス化するヘルパーメソッドを追加して、そのインスタンス をローカルに保持するようにしてください:
1 2 3 4 5 6 7 8 class FooClassTests(unittest.TestCase): def _makeContext(self, *args, **kw): return DummyContext(*args, **kw) def test_bar(self): context = self._makeContext() foo = self._makeOne(context)
このプラクティスは、結合性を回避しながら異なるテストが異なった風に モックのコンテキストを作成することを可能にします。さらに、コンテキストを 必要としないテストがコンテキスト作成の代価を払わないので、それはテストの 実行をより速くします。
モックオブジェクトを書く場合、空のクラスから初めてください。例えば:
1 2 class DummyContext: pass
テストを実行して、依存するテストを通過させるのに必要なメソッドだけを モックオブジェクトに加えてください。モックオブジェクトにテストを通過 させるのに必要ない振る舞いを与えないようにしてください。
アプリケーションが既にプラグインまたはコンポーネントを登録できるように なっている場合は、モックオブジェクトを挿入するためにその事実を利用して ください。各テストの後でクリーンアップを忘れないでください!
アプリケーションに純粋にテストの単純性を考慮したフックメソッドを追加 することも許容可能です。例えば、通常は datetime 属性を「今」にセットする コードで、 datetime.now() を直接呼ぶのではなく、モジュールスコープの 関数を使用するように調整を加えることができます。その後、テストはその 関数を既知の値を返す関数に置き替えることができます (ただし、テストが実行 された後でオリジナル版に戻す必要があります)。
AUT が依存する契約を可能な限り単純に維持することは、 AUT を書きやすくして、 また変更に対する弾力性を高めます。そのような契約の可能な限り単純な実装 だけを提供するモックオブジェクトを書くことで、AUT が「依存性の劣化 (dependency creep)」を起こさないようにします。
例えば、リレーショナルアプリケーションでは、アプリケーションによって 使用される SQL クエリは、キーワードパラメータを取って辞書のリストを返す ダミー実装としてモック化することができます:
1 2 3 4 5 6 7 8 9 10 class DummySQL: def __init__(self, results): # results should be a list of lists of dictionaries self.called_with = [] self.results = results def __call__(self, **kw): self.called_with.append(kw.copy()) return results.pop(0)
依存している契約を単純に保つ (この場合、 SQL オブジェクトが列当たり一つの マッピングをリストにして返す) ことに加えて、モックオブジェクトは、 それが AUT によってどのように使われるかを簡単にテストできるようにします:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class FooTest(unittest.TestCase): def test_barflies_returns_names_from_SQL(self): from foo.sqlregistry import registerSQL RESULTS = [[{'name': 'Chuck', 'drink': 'Guiness'}, {'name': 'Bob', 'drink': 'Knob Creek'}, ]] query = DummySQL(RESULTS[:]) registerSQL('list_barflies', query) foo = self._makeOne('Dog and Whistle') names = foo.barflies() self.assertEqual(len(names), len(RESULTS)) self.failUnless('NAME1' in names) self.failUnless('NAME2' in names) self.assertEqual(query.called_with, [{'bar': 'Dog and Whistle'}])
ここでの誘惑は、モックオブジェクトや fixture コードを別のテストモジュール から借りてくることでタイプ数を節約しようとすることです。一旦そのようなことを 許してしまうと、やがてそのような「汎用的な」 fixture を共有モジュールに 移動しようとする人が現れます。
これを禁止する根拠は、単純性です: 単体テストでは、 AUT をできるだけ明白で 単純なまま使うことが必要です。
場合によっては、同じモジュール / クラス内のテストケースメソッド (TCM) の 中でさえ fixture を共有しないようにするほうがクリーンかもしれません。
これらのルールとガイドラインに従うテストは次の特性を持ちます: