【问题标题】:Python library 'unittest': Generate multiple tests programmatically [duplicate]Python库'unittest':以编程方式生成多个测试[重复]
【发布时间】:2011-02-17 10:38:03
【问题描述】:

可能重复:
How do you generate dynamic (parameterized) unit tests in 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's advice

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()

这不起作用。运行 0 个测试。我是否错误地修改了示例?

【问题讨论】:

  • create_test 应该是一个独立的函数,而不是TestPreReqs 的方法。
  • 我猜你也无法在setUp 中安装新的测试方法,因为在调用setUp 时,测试集已经修复。 IE。您可以添加方法,它们只是不会被测试框架拾取。
  • 所以我认为上面暗示你应该把 create_test 作为一个模块级函数,然后在调用 unittest.main() 之前调用它。
  • setUp 在每次测试之前被调用。由于您没有测试,因此永远不会调用 setUp 。你可以使用 setUpModule()。

标签: python tdd unit-testing


【解决方案1】:

我不得不做类似的事情。我创建了简单的TestCase 子类,它们在__init__ 中取值,如下所示:

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

然后你可以从你的 main 方法运行它:

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

这样做的好处是:

  • 随着您添加更多值,报告的测试数量会增加,这让您感觉自己做得更多。
  • 每个单独的测试用例都可能单独失败
  • 概念上很简单,因为每个输入/输出值都转换为一个 TestCase

【讨论】:

    【解决方案2】:

    未测试:

    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)
    

    如果你经常使用它,我猜你可以使用实用函数和/或装饰器来美化它。请注意,在此示例中,pairs 不是 TestPreReqs 对象的属性(因此 setUp 消失了)。相反,它们在某种意义上与TestPreReqs 类是“硬连线”的。

    【讨论】:

    • +1。这是我在一个大型项目中成功使用的解决方案,用于比较生成时间表的系统与其预期输出。以我的经验,虽然是一个有点老套的解决方案,但它确实非常有效,因为您为每个测试都获得了一个测试用例,并且现在可以准确地找到您的测试失败的地方。
    • 这看起来很有趣,但我无法让它工作。我尝试更新了 OP。
    • 这种技术的问题在于,自动查找和运行测试的工具(例如nose)不会找到测试,因为它们在代码执行之前不存在。
    • @Dave Kirby:代码在导入时运行,所以nose 应该可以找到它。
    • 由于这个.__name__ 修复,这个答案比stackoverflow.com/a/32939/322020 更正确,但投票较少,问题被标记为重复。有点奇怪。它也较旧,我宁愿将新问题标记为重复,而不是旧问题。
    【解决方案3】:

    与 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
    

    【讨论】:

    • 由于某种原因,nosetests.selector 似乎找不到以这种方式装饰的测试。
    • FIXED:将method_for_parameter.__name__ = name_for_parameter 添加到装饰器样式中,以确保nosetest 会找到测试
    • 漂亮的东西。与鼻子相比,这种模式是我在标准库中唯一想念的东西。 :)
    • 同时将文档添加到frame.f_locals[name_for_parameter].__doc__ = method.__doc__(或method_for_parameter)允许为失败的测试打印文档字符串。
    【解决方案4】:

    nose@Paul Hankin建议)

    #!/usr/bin/env python
    # file: test_pairs_nose.py
    from nose.tools import eq_ as eq
    
    from mymodule import f
    
    def test_pairs():
        for input, output in [ (2, 332), (234, 99213), (9, 3), ]:
            yield _test_f, input, output
    
    def _test_f(input, output):
        try:
            eq(f(input), output)
        except AssertionError:
            if input == 9: # expected failure
                from nose.exc import SkipTest
                raise SkipTest("expected failure")
            else:
                raise
    
    if __name__=="__main__":
       import nose; nose.main()
    

    例子:

    $ nosetests test_pairs_nose -v
    test_pairs_nose.test_pairs(2, 332) ... ok
    test_pairs_nose.test_pairs(234, 99213) ... ok
    test_pairs_nose.test_pairs(9, 3) ... SKIP: expected failure
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    OK (SKIP=1)
    

    unittest(类似于@doublep's one的方法)

    #!/usr/bin/env python
    import unittest2 as unittest
    from mymodule import f
    
    def add_tests(generator):
        def class_decorator(cls):
            """Add tests to `cls` generated by `generator()`."""
            for f, input, output in generator():
                test = lambda self, i=input, o=output, f=f: f(self, i, o)
                test.__name__ = "test_%s(%r, %r)" % (f.__name__, input, output)
                setattr(cls, test.__name__, test)
            return cls
        return class_decorator
    
    def _test_pairs():
        def t(self, input, output):
            self.assertEqual(f(input), output)
    
        for input, output in [ (2, 332), (234, 99213), (9, 3), ]:
            tt = t if input != 9 else unittest.expectedFailure(t)
            yield tt, input, output
    
    class TestCase(unittest.TestCase):
        pass
    TestCase = add_tests(_test_pairs)(TestCase)
    
    if __name__=="__main__":
        unittest.main()
    

    例子:

    $ python test_pairs_unit2.py -v
    test_t(2, 332) (__main__.TestCase) ... ok
    test_t(234, 99213) (__main__.TestCase) ... ok
    test_t(9, 3) (__main__.TestCase) ... expected failure
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.000s
    
    OK (expected failures=1)
    

    如果您不想安装unittest2,请添加:

    try:
        import unittest2 as unittest
    except ImportError:
        import unittest
        if not hasattr(unittest, 'expectedFailure'):
           import functools
           def _expectedFailure(func):
               @functools.wraps(func)
               def wrapper(*args, **kwargs):
                   try:
                       func(*args, **kwargs)
                   except AssertionError:
                       pass
                   else:
                       raise AssertionError("UnexpectedSuccess")
               return wrapper
           unittest.expectedFailure = _expectedFailure
    

    【讨论】:

      【解决方案5】:

      一些可用于在 Python 中进行参数化测试的工具是:

      有关此问题的更多答案,另请参阅question 1676269

      【讨论】:

        【解决方案6】:

        我认为Rory's solution 是最干净和最短的。但是,doublep's“在 TestCase 中创建合成函数”的这种变体也可以:

        from functools import partial
        
        class TestAllReports(unittest.TestCase):
            pass
        
        def test_spamreport(name):
            assert classify(getSample(name))=='spamreport', name
        
        for rep in REPORTS:
            testname = 'test_' + rep
            testfunc = partial(test_spamreport, rep)
            testfunc.__doc__ = testname
            setattr(TestAllReports, testname, testfunc)
        
        if __name__=='__main__':
            unittest.main(argv=sys.argv + ['--verbose'])
        

        【讨论】:

          猜你喜欢
          • 2013-01-01
          • 2014-05-24
          • 1970-01-01
          • 1970-01-01
          • 2017-07-27
          • 2012-10-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-13
          相关资源
          最近更新 更多