unittest和 metaclass:自动生成 test_* 方法

4 投票
2 回答
1779 浏览
提问于 2025-04-16 12:52

在我为一个框架创建测试时,我开始注意到一个模式:

class SomeTestCase(unittest.TestCase):

    def test_feat_true(self):
        _test_feat(self, True)

    def test_feat_false(self):
        _test_feat(self, False)

    def _test_feat(self, arg):
        pass    # test logic goes here

所以我想用一个元类来程序化地创建 test_feat_* 方法,适用于这些类型的测试类。换句话说,对于每个私有方法,签名是 _test_{featname}(self, arg),我想要创建两个顶层的、可以被发现的方法,签名分别是 test_{featname}_true(self)test_{featname}_false(self)

我想出了类似这样的东西:

#!/usr/bin/env python

import unittest


class TestMaker(type):

    def __new__(cls, name, bases, attrs):
        callables = dict([
            (meth_name, meth) for (meth_name, meth) in attrs.items() if
            meth_name.startswith('_test')
        ])

        for meth_name, meth in callables.items():
            assert callable(meth)
            _, _, testname = meth_name.partition('_test')

            # inject methods: test{testname}_{[false,true]}(self)
            for suffix, arg in (('false', False), ('true', True)):
                testable_name = 'test{0}{1}'.format(testname, suffix)
                attrs[testable_name] = lambda self: meth(self, arg)

        return type.__new__(cls, name, bases, attrs)


class TestCase(unittest.TestCase):

    __metaclass__ = TestMaker

    def _test_this(self, arg):
        print 'this: ' + str(arg)

    def _test_that(self, arg):
        print 'that: ' + str(arg)


if __name__ == '__main__':
    unittest.main()

我期待得到一些像这样的输出:

this: False
this: True
that: False
that: True

但我得到的是:

$ ./test_meta.py
that: True
.that: True
.that: True
.that: True
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

看起来我遗漏了一些闭包规则。我该如何解决这个问题?有没有更好的方法?

谢谢,

编辑:已修复。请查看:这个代码片段

2 个回答

1

与其走元类的复杂路线,我建议你可以看看使用nose测试生成器来处理这类问题:

http://somethingaboutorange.com/mrl/projects/nose/1.0.0/writing_tests.html#test-generators

使用测试生成器的缺点是它们是nose特有的功能,所以你需要引入一个标准库以外的依赖。不过,优点是我觉得它们更容易编写和理解。

5

确实,这是一个闭包的问题:

attrs[testable_name] = lambda self: meth(self, arg)

改成

attrs[testable_name] = lambda self,meth=meth,arg=arg: meth(self, arg)

通过使用默认值,arg 在每次循环中都绑定到那个默认值,这样每次循环的arg都是不同的。如果没有默认值,arg 就会在循环结束后变成最后一次循环的值。(meth 也是一样的情况。)

撰写回答