python 2.6: 僕と unittest と時々 unittest2
testing.mysqld に @skipIfNotInstalled というデコレータを追加しました。
名前の通り、MySQL がインストールされていない場合にテストをスキップするというものです。
もちろん testing.postgresql にも入っています。(testing.cassandra はこのあと手がけます)
MySQL が必要なテストが限られているのであれば、個々人の開発環境に MySQL がなくてもテストが実行できる方が幸せですもんね。
僕の python2.6 には skipIf がない
@skipIfNotInstalled は内部で unittest.skipIf を使っているのですが、
unittest.skipIf は python2.7 から導入されたものであるため python 2.6 では利用することができません。
あたらしい unittest では、他にもいくつも便利な機能が導入されているので、python 2.6 以前でテストコードを書く際は unittest2 を利用することをオススメします。
そこで登場するのが unittest2 パッケージです。
unittest2 は python 2.x 系にあたらしい unittest の機能を提供するバックポートパッケージの役割を持っています。
そのため、python2.6 でも unittest2.skipIf を使えばテストのスキップを実現できるということです。
なお、unittest2 は python 2.x 用のパッケージであるため python 3.x 系では利用できません。
そのため、どちらの環境でもテストコードを動かすためには次のようにして unittest と unittest2 を切り替えるコードを書く必要があります。
import sys if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest @unittest.skipIf(...) class MyTestCase(unittest.TestCase): ...
unittest と unittest2 が混在した世界
さて、sys.version_info を使って unittest と unittest2 を切り替えることに成功したのですが、
このふたつを混在させるとトラブルが生じます。
今回作った @skipIfNotInstalled を使ってみましょう。
import unittest import testing.mysqld @testing.mysqld.skipIfNotInstalled class MyTestCase(unittest.TestCase): def setUp(self): self.mysqld = testing.mysqld.Mysqld(my_cnf={'skip-networking': None}) ...
一見なんの変哲もないテストコードなのですが、一点問題があります。
このコードを python 2.6 で動かした時、@skipIfNotInstalled は内部的に unittest2 の skipIf を呼んでいるのですが、
このテストコードでは python 2.6 に含まれる unittest (2 ではない) を使ってテストが実装されているのです。
ここで食い違いが発生するため、テストを動作させるとテストがスキップされずにエラーが発生します。
手元では python2.6 setup.py test や nose を使ってテストを実行させる際に以下のエラーが発生しました。
Traceback (most recent call last): File "/Users/tkomiya/work/schema2rst/nose-1.3.0-py2.6.egg/nose/case.py", line 267, in setUp try_run(self.test, names) File "/Users/tkomiya/work/schema2rst/nose-1.3.0-py2.6.egg/nose/util.py", line 469, in try_run return func() TypeError: setUp() takes exactly 1 argument (0 given)
この問題は unittest と unittest2 の食い違いによって起きているため、
python 2.6 の場合には次のように unittest2 を利用すると問題が解決します。
import sys if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest import testing.mysqld @testing.mysqld.skipIfNotInstalled class MyTestCase(unittest.TestCase): def setUp(self): self.mysqld = testing.mysqld.Mysqld(my_cnf={'skip-networking': None}) ...
なんでこんなこと起きてるのさ?
unittest, unittest2 では次のような関数でテストのスキップを実装しています。
def skip(reason): """ Unconditionally skip a test. """ def decorator(test_item): if not (isinstance(test_item, type) and issubclass(test_item, TestCase)): @wraps(test_item) def skip_wrapper(*args, **kwargs): raise SkipTest(reason) test_item = skip_wrapper test_item.__unittest_skip__ = True test_item.__unittest_skip_why__ = reason return test_item return decorator
ぱっと見では分かりづらいのですが
- テスト対象(test_item)に __unittest_skip__ フラグを立てている
- テスト対象が TestCase クラスのサブクラスではない場合(関数やメソッドなど)は skip_wrapper 関数でラッピングしている
という実装になっています。
ここで問題になるのは「テスト対象が TestCase クラスのサブクラスではない場合」という部分です。
TestCase クラスを継承している場合はこの条件に一致しないため、
skip_wrapper 関数でラッピングされず skip() 関数からはクラスそのものが返ります。
この条件は unittest2 では「テスト対象が unittest2.TestCase クラスのサブクラスではない場合」という風に解釈されるため、
unittest.TestCase (2 ではない)を継承している場合はこの条件に「一致してしま」います。
そのため、skip() 関数の返り値が関数になってしまいます。
先ほど引用したエラーで「TypeError: setUp() takes exactly 1 argument (0 given)」というエラーが出ているのは、
この違いによって setUp() が関数として扱われてしまっている(メソッドではなくなった)ために起きてしまったものでした。
まとめ
- python2.6 でテストを書く際は unittest2 おすすめ
- でも、unittest と unittest2 が混在するとハマることがある。とりあえず skipIf まわり。
- 僕の午前中を返せ。