2011-10-13

@tokibito さんの TestBedTestCase をいじる

@tokibito さんが、Google App Engine のスタブを使う TestCase クラスを紹介しています。私の仕事でも使っています。

サブクラスを作ったときに、TestBedTestCase の setUp と tearDown を明示的に呼び出さないといけないのがちょっと面倒です。

class MyTestCase(TestBedTestCase):
    def setUp(self):
        super(MyTestCase, class).setUp()
        # … 準備 ...

    def tearDown(self):
        # … 片付け …
        super(MyTestCase, class).tearDown()

Pythonic なので、これはこれで別に問題ないんだろうけど、これ結構めんどくさい。あと、nose 使えば、モジュールごとに setUp を定義しておくと、こういうのもやってくれるはずだけど、老害じじいは nose とか分かんないので、とりあえずスルー。

という訳で、ベースクラスの setUp と tearDown を自動的に呼び出すようにして使っています。setUp の呼び出し順は、基底クラスの setUp の後で、派生クラスの setUp。tearDown は逆です。それぞれ、C++ なんかのコンストラクタとデストラクタと同じにしました。

class CascadingTestCaseMeta(type):
    """Metaclass for TestCase class

    This metaclass make setUp method calls all setUp methods 
    in base classes and then calls defined setUp method.  
    Likewise tearDown but in opposite order.
    """

    def __init__(cls, name, bases, ns):
        for method_name, reverse in [('setUp',False), ('tearDown', True)]:
            setattr(cls, method_name,
                    cls._create_method(method_name, bases, ns, reverse=False))

    @classmethod
    def _create_method(self, method_name, bases, ns, reverse=True):
        """return a method that calls all methods with given 
        name in class hierarchy
        """
        # create method sequence in parent and current classes
        methods = [getattr(base, method_name, lambda self: None)
                   for base in bases]
        methods.append(ns.get(method_name, lambda self: None))
        # reverse order if necessary
        if reverse:
            methods.reverse()
        # define method to call all methods
        def call_methods(self):
            for method in methods:
                method(self)
        # return the caller method
        return call_methods

class CascadingTestCase(unittest.TestCase):
    __metaclass__ = CascadingTestCaseMeta

class TestBedTestCase(CascadingTestCase):
    # 以下、@tokibito さんのコード

TestBedTestCase のメタクラスを直接指定していないのは、testbed の setUp よりも「前に」何かしたいときと、「後に」何かしたいときがあると思ったからです(でも今のところない…)。

class MyTestCase(TestBedTestCase, MyBaseTestCase):
    def setUp(self):
        ...

と定義すると、TestBedTestCase.setUp, MyBase.setUp, MyTestCase.setUp の順に呼び出されます。

TestBedTestCase のsetUp と tearDown が呼び出されるのが明示的じゃない気がしますが、そこの明示性は諦めました。

やっぱ nose かなぁ…。