python 编写可重用(参数化)的 unittest.TestCase 方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1676269/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 22:51:03  来源:igfitidea点击:

Writing a re-usable (parametrized) unittest.TestCase method

pythonunit-testingtestcaseparameterized-unit-test

提问by astrofrog

Possible Duplicate:
How to generate dynamic (parametrized) unit tests in python?

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

I'm writing tests using the unittest package, and I want to avoid repeated code. I am going to carry out a number of tests which all require a very similar method, but with only one value different each time. A simplistic and useless example would be:

我正在使用 unittest 包编写测试,我想避免重复代码。我将进行许多测试,这些测试都需要非常相似的方法,但每次只有一个值不同。一个简单而无用的例子是:

class ExampleTestCase(unittest.TestCase):

    def test_1(self):
        self.assertEqual(self.somevalue, 1)

    def test_2(self):
        self.assertEqual(self.somevalue, 2)

    def test_3(self):
        self.assertEqual(self.somevalue, 3)

    def test_4(self):
        self.assertEqual(self.somevalue, 4)

Is there a way to write the above example without repeating all the code each time, but instead writing a generic method, e.g.

有没有办法编写上面的例子,而不必每次都重复所有的代码,而是编写一个通用的方法,例如

    def test_n(self, n):
        self.assertEqual(self.somevalue, n)

and telling unittest to try this test with different inputs?

并告诉 unittest 用不同的输入来尝试这个测试?

回答by akaihola

Some of the tools available for doing parametrized tests in Python are:

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

回答by Andrew Dalke

If you really want to have multiple unitttest then you need multiple methods. The only way to get that is through some sort of code generation. You can do that through a metaclasses, or by tweaking the class after the definition, including (if you are using Python 2.6) through a class decorator.

如果你真的想有多个 unitttest 那么你需要多种方法。获得它的唯一方法是通过某种代码生成。您可以通过元类或通过在定义之后调整类来做到这一点,包括(如果您使用的是 Python 2.6)通过类装饰器。

Here's a solution which looks for the special 'multitest' and 'multitest_values' members and uses those to build the test methods on the fly. Not elegant, but it does roughly what you want:

这是一个解决方案,它寻找特殊的“multitest”和“multitest_values”成员,并使用这些成员动态构建测试方法。不优雅,但它大致可以满足您的需求:

import unittest
import inspect

class SomeValue(object):
    def __eq__(self, other):
        return other in [1, 3, 4]

class ExampleTestCase(unittest.TestCase):
    somevalue = SomeValue()

    multitest_values = [1, 2, 3, 4]
    def multitest(self, n):
        self.assertEqual(self.somevalue, n)

    multitest_gt_values = "ABCDEF"
    def multitest_gt(self, c):
        self.assertTrue(c > "B", c)


def add_test_cases(cls):
    values = {}
    functions = {}
    # Find all the 'multitest*' functions and
    # matching list of test values.
    for key, value in inspect.getmembers(cls):
        if key.startswith("multitest"):
            if key.endswith("_values"):
                values[key[:-7]] = value
            else:
                functions[key] = value

    # Put them together to make a list of new test functions.
    # One test function for each value
    for key in functions:
        if key in values:
            function = functions[key]
            for i, value in enumerate(values[key]):
                def test_function(self, function=function, value=value):
                    function(self, value)
                name ="test%s_%d" % (key[9:], i+1)
                test_function.__name__ = name
                setattr(cls, name, test_function)

add_test_cases(ExampleTestCase)

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

This is the output from when I run it

这是我运行时的输出

% python stackoverflow.py
.F..FF....
======================================================================
FAIL: test_2 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "stackoverflow.py", line 34, in test_function
    function(self, value)
  File "stackoverflow.py", line 13, in multitest
    self.assertEqual(self.somevalue, n)
AssertionError: <__main__.SomeValue object at 0xd9870> != 2

======================================================================
FAIL: test_gt_1 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "stackoverflow.py", line 34, in test_function
    function(self, value)
  File "stackoverflow.py", line 17, in multitest_gt
    self.assertTrue(c > "B", c)
AssertionError: A

======================================================================
FAIL: test_gt_2 (__main__.ExampleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "stackoverflow.py", line 34, in test_function
    function(self, value)
  File "stackoverflow.py", line 17, in multitest_gt
    self.assertTrue(c > "B", c)
AssertionError: B

----------------------------------------------------------------------
Ran 10 tests in 0.001s

FAILED (failures=3)

You can immediately see some of the problems which occur with code generation. Where does "test_gt_1" come from? I could change the name to the longer "test_multitest_gt_1" but then which test is 1? Better here would be to start from _0 instead of _1, and perhaps in your case you know the values can be used as a Python function name.

您可以立即看到代码生成中出现的一些问题。“test_gt_1”从何而来?我可以将名称更改为更长的“test_multitest_gt_1”,但是哪个测试是 1?这里更好的是从 _0 而不是 _1 开始,也许在您的情况下,您知道这些值可以用作 Python 函数名称。

I do not like this approach. I've worked on code bases which autogenerated test methods (in one case using a metaclass) and found it was much harder to understand than it was useful. When a test failed it was hard to figure out the source of the failure case, and it was hard to stick in debugging code to probe the reason for the failure.

我不喜欢这种方法。我研究了自动生成测试方法的代码库(在一种情况下使用元类),发现它比有用更难理解。当测试失败时,很难找出失败案例的来源,也很难坚持调试代码来探查失败的原因。

(Debugging failures in the example I wrote here isn't as hard as that specific metaclass approach I had to work with.)

(在我在这里编写的示例中调试失败并不像我必须使用的特定元类方法那么难。)

回答by anonymous

I guess what you want is "parameterized tests".

我想你想要的是“参数化测试”。

I don't think unittest module supports this (unfortunately), but if I were adding this feature it would look something like this:

我不认为 unittest 模块支持这个(不幸的是),但如果我添加这个功能,它看起来像这样:

# Will run the test for all combinations of parameters
@RunTestWith(x=[0, 1, 2, 3], y=[-1, 0, 1])
def testMultiplication(self, x, y):
  self.assertEqual(multiplication.multiply(x, y), x*y)

With the existing unittest module, a simple decorator like this won't be able to "replicate" the test multiple times, but I think this is doable using a combination of a decorator and a metaclass (metaclass should observe all 'test*' methods and replicate (under different auto-generated names) those that have a decorator applied).

使用现有的 unittest 模块,像这样的简单装饰器将无法多次“复制”测试,但我认为使用装饰器和元类的组合是可行的(元类应该观察所有 'test*' 方法并复制(在不同的自动生成的名称下)应用了装饰器的那些)。

回答by intuited

A more data-oriented approach might be clearer than the one used in Andrew Dalke's answer:

一种更面向数据的方法可能比Andrew Dalke回答中使用的方法更清晰:

"""Parametrized unit test.

Builds a single TestCase class which tests if its
  `somevalue` method is equal to the numbers 1 through 4.

This is accomplished by
  creating a list (`cases`)
  of dictionaries which contain test specifications
  and then feeding the list to a function which creates a test case class.

When run, the output shows that three of the four cases fail,
  as expected:

>>> import sys
>>> from unittest import TextTestRunner
>>> run_tests(TextTestRunner(stream=sys.stdout, verbosity=9))
... # doctest: +ELLIPSIS
Test if self.somevalue equals 4 ... FAIL
Test if self.somevalue equals 1 ... FAIL
Test if self.somevalue equals 3 ... FAIL
Test if self.somevalue equals 2 ... ok
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 4
----------------------------------------------------------------------
Traceback (most recent call last):
  ...
AssertionError: 2 != 4
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 1
----------------------------------------------------------------------
Traceback (most recent call last):
  ...
AssertionError: 2 != 1
<BLANKLINE>
======================================================================
FAIL: Test if self.somevalue equals 3
----------------------------------------------------------------------
Traceback (most recent call last):
  ...
AssertionError: 2 != 3
<BLANKLINE>
----------------------------------------------------------------------
Ran 4 tests in ...s
<BLANKLINE>
FAILED (failures=3)
"""

from unittest import TestCase, TestSuite, defaultTestLoader

cases = [{'name': "somevalue_equals_one",
          'doc': "Test if self.somevalue equals 1",
          'value': 1},
         {'name': "somevalue_equals_two",
          'doc': "Test if self.somevalue equals 2",
          'value': 2},
         {'name': "somevalue_equals_three",
          'doc': "Test if self.somevalue equals 3",
          'value': 3},
         {'name': "somevalue_equals_four",
          'doc': "Test if self.somevalue equals 4",
          'value': 4}]

class BaseTestCase(TestCase):
    def setUp(self):
        self.somevalue = 2

def test_n(self, n):
    self.assertEqual(self.somevalue, n)

def make_parametrized_testcase(class_name, base_classes, test_method, cases):
    def make_parametrized_test_method(name, value, doc=None):
        def method(self):
            return test_method(self, value)
        method.__name__ = "test_" + name
        method.__doc__ = doc
        return (method.__name__, method)

    test_methods = (make_parametrized_test_method(**case) for case in cases)
    class_dict = dict(test_methods)
    return type(class_name, base_classes, class_dict)


TestCase = make_parametrized_testcase('TestOneThroughFour',
                                      (BaseTestCase,),
                                      test_n,
                                      cases)

def make_test_suite():
    load = defaultTestLoader.loadTestsFromTestCase
    return TestSuite(load(TestCase))

def run_tests(runner):
    runner.run(make_test_suite())

if __name__ == '__main__':
    from unittest import TextTestRunner
    run_tests(TextTestRunner(verbosity=9))

I'm not sure what voodoo is involved in determining the order in which the tests are run, but the doctest passes consistently for me, at least.

我不确定在确定测试运行的顺序时涉及什么巫毒,但至少 doctest 对我来说始终如一地通过。

For more complex situations it's possible to replace the valueselement of the casesdictionaries with a tuple containing a list of arguments and a dict of keyword arguments. Though at that point you're basically coding lisp in python.

对于更复杂的情况,可以用包含参数列表和关键字参数字典的元组替换字典的values元素cases。虽然那时你基本上是在 python 中编码 lisp。

回答by Robert Rossney

Write a single test method that performs all of your tests and captures all of the results, write your own diagnostic messages to stderr, and fail the test if any of its subtests fail:

编写一个测试方法来执行所有测试并捕获所有结果,将您自己的诊断消息写入 stderr,如果任何子测试失败,则测试失败:

def test_with_multiple_parameters(self):
    failed = False
    for k in sorted(self.test_parameters.keys()):
        if not self.my_test(self.test_parameters[k]):
           print >> sys.stderr, "Test {0} failed.".format(k)
           failed = True
    self.assertFalse(failed)            

Note that of course my_test()'s name can't begin with test.

请注意,当然my_test()的名称不能以test.

回答by pojo

Perhaps something like:

也许是这样的:

def test_many(self):
    for n in range(0,1000):
        self.assertEqual(self.somevalue, n)