Pythonで簡単な単体テストをはじめよう - doctest

名前は知ってたけどなんとなく使う気にならなかったdoctestだが,今日ライブラリリファレンスを眺めていたら,やはりなんとなく使ってみたくなったので動かしてみた.そして気づいた.これは面白いし便利だ!
動作の気になるモジュールは今まで下のようにテストしてたんだけど,

class Hoge(object):
  def foo(self, num):
    return [e for e in range(num)]

def bar(num):
  print num * 10

def _test()
  a = Hoge()
  print a.foo()
  bar(10)

if __name__ == '__main__':
  _test()

これだと結果を見て中身を目視で確認する必要がある.原始的なテスト方法だね.一方,doctestを使うと各関数内に書いてあるドキュメンテーション文字列(関数やクラスの動作を説明するテキスト)内から自動的にテストっぽい文字列を探してテストを実行してくれるのだ.当然テスト結果は自動的に評価して,どのテストが上手くいってどのテストが失敗したかをレポートしてくれる.

class Hoge(object):
  def foo(self, num):
    """
    0〜num-1までの数字をリスト形式で作成する
    >>> a = Hoge()
    >>> a.foo(5)
    [0, 1, 2, 3, 4]
    """
    return [e for e in range(num)]

def bar(num):
  """
  numの10倍の値を表示する
  >>> bar(10)
  100
  """
  print num * 100

def _test():
  import doctest
  doctest.testmod()

if __name__ == "__main__":
  _test()

bar関数は明らかにコメントと実装が違ってるので失敗する.テストに失敗した場合はこんな感じに期待した値と実際の値が表示される.

******************************************
File "test.py", line 14, in __main__.puts
Failed example:
    bar(10)
Expected:
    100
Got:
    1000
******************************************
1 items had failures:
   1 of   1 in __main__.puts
***Test Failed*** 1 failures.

最初の原始的なテスト方法に比べて嬉しい点は二つ.

  • テスト結果を自動的に評価してくれる
  • テストがそのまま関数の使い方の説明になる

一つ目はいわゆる単体テストフレームワークなら当然あってしかるべきなんだけど,二つ目はどうだろう?他の言語のテストフレームワークだとなかなか難しいのではないだろうか.

二つ目の利点のおかげでテストを書いているという意識を少し軽くしてくれるのが素敵だ.僕みたいな趣味プログラマな人はテストってあまり書かないと思うけど(?),少なくともコメントは残すはず.使い方を日本語で書く代わりにドキュメンテーション文字列にちょちょっと書いておけばテストにもなるんだから,テスト作成の敷居も下がること間違いなし.

コメント内に書くという性質上,冗長なテストを作ってしまうとソースが見づらくなるので,自然とシンプルで良いテストが作れるようになるというメリットもあるかも.

ちなみに,上の例で分かるとおり,テストはそのモジュール内のグローバルな名前空間で実行される.つまり,テスト開始段階でそれぞれのプログラムの中で使えるユーザ定義の名前は"_test, bar, Hoge"だけだ.

少しずつテスト作りに慣れていこう.

関連モジュールにunittestというのがあるけど,こっちは他の言語のテストフレームワークに近いっぽい.いちいちテスト用のクラスを定義するのは面倒くさいのだ…

おまけ

期待する答えとテスト結果は空白の数まで厳密に比較されるので,リストや辞書を返す関数を作る場合は結果を正確に記述するのが大変.下のように自分でリストとか辞書を作って比較すると良い.

def baz(num):
  """
  >>> baz(5) == [0,1,2,3,4]
  True
  """
  return [e for e in range(num)]

def fuga(key_list, value):
  """
  >>> fuga(["a","b","c"], 1) == {"a":1,"b":1,"c":1}
  True
  """
  d = {}
  for key in key_list:
    d[key] = value
  return d