21 Dec 2013

効果的な unittest の指針

こちらを読んだ。

効果的なunittest - または、callFUTの秘密

完結で高速で依存が少ないテストを書くための指針で、以下の項目があげられている。

  • ルール: テスト対象のモジュール(module-under-test)をテストモジュールに直接importしない
  • ガイドライン: モジュールスコープでの依存を最小限にする
  • ルール: 各テストメソッドでは、1つの事実だけを確認する
  • ルール: テストメソッドは内容を表すようにしよう
  • ガイドライン: setupはヘルパーメソッドで提供しよう。テストケースのselfで共有するのはやめよう。
  • ガイドライン: フィクスチャは可能な限り簡潔に
  • ガイドライン: フックやレジストリなどの利用は注意深く
  • ガイドライン: 依存関係を明確にするためにモックを利用する
  • ルール: テストモジュール間でテキストを共有しない

個人的に面白かったのは setupはヘルパーメソッドで提供しよう。テストケースのselfで共有するのはやめよう。 というガイドライン。例を引用すると、

class FooClassTests(unittest.TestCase):

   def setUp(self):
       self.context = DummyContext()

   # ...

   def test_bar(self):
       foo = self._makeOne(self.context)

このようにテストクラスのメンバとして必要なデータを保持するのではなく、

class FooClassTests(unittest.TestCase):

   def _makeContext(self, *args, **kw):
       return DummyContext(*args, **kw)

   def test_bar(self):
       context = self._makeContext()
       foo = self._makeOne(self.context)

このようにヘルパメソッドを用意して必要なときだけダミーを作るという方針だ。こうすることで依存が下げられるし本当に必要なときだけ DummyContext が作られるので高速化にもつながるというわけだ。

自分はこれを “テストコードをどれだけ DRY に書くか” という問題で、思いっきり “非DRY” のほうに倒しているととらえた。

しばしばテストコードは DRY でなく冗長に書いたほうが良い場合がある。例えば入力と期待結果が数種類あって、それらの適用方法が全く同じ場合。入力と期待結果を配列でもっておいてひとつのテストメソッド内でループでまわすよりも、それぞれ個別のテストメソッドでひとつづず記述したほうが、fail した際の原因特定やテスト対象の仕様の明確化の観点から良い場合がある。(Assertion Roulette と呼ばれているパターンがそれかな?)

“モジュールスコープでの依存を最小限にする”、”テストモジュール間でテキストを共有しない” あたりも同じように、”DRY を崩してでも必要最低限なものだけに絞るほうに極力倒そう” というのが基本方針にあると思う。テストコードはプロダクションのコードとは違って、DRY にして変更に強くすることを求めるよりも、明瞭で依存が少なく高速に動くことを第一に持ってきたほうより旨味があるという考え方なんだろう。

  • テスト対象でないモジュールのロードは必要なメソッドだけに絞る
  • setup はヘルパーメソッドに
  • フィクスチャは限りなく簡潔に。また共用はできるだけ避ける

たしかにあまり意識せずにいると自然とこれらの逆のテストコードを書いてしまいがちなので、意識してそうならないように、本当に必要な場合にだけ共通部分をまとめるようにすると良いのかもしれない。このトレードオフはいつも悩むところで、もっと掘り下げた考察も読みたいなと思いつつ、今後ちょっと気にかけてみようと思った。

ほかの “各テストメソッドでは、1つの事実だけを確認する”、”テストメソッドは内容を表すようにしよう” あたりのルールについてはその通りだし、特にトレードオフもなく常にこの方針をとるべきものだと思う。