Python库'unittest':程序matically生成多个测试

57 投票
6 回答
43070 浏览
提问于 2025-04-15 22:31

可能重复的问题:
如何在Python中生成动态(参数化)单元测试?

我有一个需要测试的函数,叫做 under_test,还有一组预期的输入/输出对:

[
(2, 332),
(234, 99213),
(9, 3),
# ...
]

我希望每一个输入/输出对都能在自己的 test_* 方法中进行测试。这可能吗?

这就是我想要的,但我把每一个输入/输出对都强行放进了一个测试里:

class TestPreReqs(unittest.TestCase):

    def setUp(self):
        self.expected_pairs = [(23, 55), (4, 32)]

    def test_expected(self):
        for exp in self.expected_pairs:
            self.assertEqual(under_test(exp[0]), exp[1])

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

(另外,我真的想把 self.expected_pairs 的定义放在 setUp 里吗?)

更新:尝试了 doublep 的建议

class TestPreReqs(unittest.TestCase):

    def setUp(self):
        expected_pairs = [
                          (2, 3),
                          (42, 11),
                          (3, None),
                          (31, 99),
                         ]

        for k, pair in expected_pairs:
            setattr(TestPreReqs, 'test_expected_%d' % k, create_test(pair))

    def create_test (pair):
        def do_test_expected(self):
            self.assertEqual(get_pre_reqs(pair[0]), pair[1])
        return do_test_expected


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

这个方法不行。没有运行任何测试。我是不是把例子改错了?

6 个回答

27

在Python中,常常有复杂的方法来解决简单的问题。

在这种情况下,我们可以使用元编程、装饰器和一些巧妙的Python技巧来达到一个不错的效果。下面是最终测试的样子:

import unittest

# Some magic code will be added here later

class DummyTest(unittest.TestCase):
  @for_examples(1, 2)
  @for_examples(3, 4)
  def test_is_smaller_than_four(self, value):
    self.assertTrue(value < 4)

  @for_examples((1,2),(2,4),(3,7))
  def test_double_of_X_is_Y(self, x, y):
    self.assertEqual(2 * x, y)

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

当执行这个脚本时,结果是:

..F...F
======================================================================
FAIL: test_double_of_X_is_Y(3,7)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example
    method(self, *example)
  File "/Users/xdecoret/Documents/foo.py", line 41, in test_double_of_X_is_Y
    self.assertEqual(2 * x, y)
AssertionError: 6 != 7

======================================================================
FAIL: test_is_smaller_than_four(4)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example
    method(self, *example)
  File "/Users/xdecoret/Documents/foo.py", line 37, in test_is_smaller_than_four
    self.assertTrue(value < 4)
AssertionError

----------------------------------------------------------------------
Ran 7 tests in 0.001s

FAILED (failures=2)

这达成了我们的目标:

  • 它不干扰:我们像往常一样从TestCase继承
  • 我们只需编写一次参数化测试
  • 每个示例值都被视为一个独立的测试
  • 装饰器可以叠加使用,所以很容易使用一组示例(例如,使用一个函数从示例文件或目录中构建值列表)
  • 最棒的是,它可以处理任意数量的参数

那么它是怎么工作的呢?基本上,装饰器会把示例存储在函数的一个属性中。我们使用元类来把每个被装饰的函数替换成一个函数列表。然后我们用我们的新魔法代码替换unittest.TestCase(这段代码要粘贴到上面的“魔法”注释中):

__examples__ = "__examples__"

def for_examples(*examples):
    def decorator(f, examples=examples):
      setattr(f, __examples__, getattr(f, __examples__,()) + examples)
      return f
    return decorator

class TestCaseWithExamplesMetaclass(type):
  def __new__(meta, name, bases, dict):
    def tuplify(x):
      if not isinstance(x, tuple):
        return (x,)
      return x
    for methodname, method in dict.items():
      if hasattr(method, __examples__):
        dict.pop(methodname)
        examples = getattr(method, __examples__)
        delattr(method, __examples__)
        for example in (tuplify(x) for x in examples):
          def method_for_example(self, method = method, example = example):
            method(self, *example)
          methodname_for_example = methodname + "(" + ", ".join(str(v) for v in example) + ")"
          dict[methodname_for_example] = method_for_example
    return type.__new__(meta, name, bases, dict)

class TestCaseWithExamples(unittest.TestCase):
  __metaclass__ = TestCaseWithExamplesMetaclass
  pass

unittest.TestCase = TestCaseWithExamples

如果有人想把这个打包得更好,或者为unittest提个补丁,随意哦!提到我的名字会很感谢。

如果你愿意使用框架的反射(导入sys模块),代码可以变得更简单,并且完全封装在装饰器中。

def for_examples(*parameters):

  def tuplify(x):
    if not isinstance(x, tuple):
      return (x,)
    return x

  def decorator(method, parameters=parameters):
    for parameter in (tuplify(x) for x in parameters):

      def method_for_parameter(self, method=method, parameter=parameter):
        method(self, *parameter)
      args_for_parameter = ",".join(repr(v) for v in parameter)
      name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
      frame = sys._getframe(1)  # pylint: disable-msg=W0212
      frame.f_locals[name_for_parameter] = method_for_parameter
    return None
  return decorator
57

我之前也做过类似的事情。我创建了一些简单的 TestCase 子类,这些子类在初始化的时候会接收一个值,像这样:

class KnownGood(unittest.TestCase):
    def __init__(self, input, output):
        super(KnownGood, self).__init__()
        self.input = input
        self.output = output
    def runTest(self):
        self.assertEqual(function_to_test(self.input), self.output)

然后我用这些值创建了一个测试套件:

def suite():
    suite = unittest.TestSuite()
    suite.addTests(KnownGood(input, output) for input, output in known_values)
    return suite

你可以从你的主方法中运行它:

if __name__ == '__main__':
    unittest.TextTestRunner().run(suite())

这样做的好处有:

  • 当你添加更多的值时,报告的测试数量会增加,这样会让你觉得自己做了更多的工作。
  • 每个单独的测试用例都可以单独失败。
  • 这个概念很简单,因为每个输入/输出值都被转换成一个 TestCase。
41

没有测试:

class TestPreReqs(unittest.TestCase):
    ...

def create_test (pair):
    def do_test_expected(self):
        self.assertEqual(under_test(pair[0]), pair[1])
    return do_test_expected

for k, pair in enumerate ([(23, 55), (4, 32)]):
    test_method = create_test (pair)
    test_method.__name__ = 'test_expected_%d' % k
    setattr (TestPreReqs, test_method.__name__, test_method)

如果你经常使用这个,可以考虑通过一些工具函数或者装饰器来让它看起来更好。我想说的是,在这个例子中,成对的东西并不是TestPreReqs对象的一个属性(所以setUp就没有了)。实际上,它们在某种程度上是“硬编码”到TestPreReqs类里的。

撰写回答