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

Python 编写可重用(参数化)的unittest.TestCase方法,python,unit-testing,testcase,parameterized-unit-test,Python,Unit Testing,Testcase,Parameterized Unit Test,可能重复: 我正在使用unittest包编写测试,我希望避免重复代码。我将进行一些测试,这些测试都需要一个非常相似的方法,但每次只有一个值不同。一个简单而无用的例子是: class ExampleTestCase(unittest.TestCase): def test_1(self): self.assertEqual(self.somevalue, 1) def test_2(self): self.assertEqual(self.so

可能重复:

我正在使用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)
有没有一种方法可以编写上面的示例而不必每次重复所有代码,而是编写一个通用方法,例如

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

并告诉unittest使用不同的输入尝试此测试?

可能类似于:

def test_many(self):
    for n in range(0,1000):
        self.assertEqual(self.somevalue, n)
我想你想要的是“参数化测试”

我不认为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)

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

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

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

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()
这是我运行它时的输出

% 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)
%python stackoverflow.py
.F..FF。。。。
======================================================================
失败:测试2(主测试用例)
----------------------------------------------------------------------
回溯(最近一次呼叫最后一次):
test_函数中的第34行文件“stackoverflow.py”
功能(自我、价值)
文件“stackoverflow.py”,第13行,在multitest中
self.assertEqual(self.somevalue,n)
断言者错误:!=2.
======================================================================
失败:测试\u gt\u 1(\u主\u。示例TestCase)
----------------------------------------------------------------------
回溯(最近一次呼叫最后一次):
test_函数中的第34行文件“stackoverflow.py”
功能(自我、价值)
文件“stackoverflow.py”,第17行,在multitest\u gt
self.assertTrue(c>“B”,c)
断言者:一个
======================================================================
失败:测试2(主测试用例)
----------------------------------------------------------------------
回溯(最近一次呼叫最后一次):
test_函数中的第34行文件“stackoverflow.py”
功能(自我、价值)
文件“stackoverflow.py”,第17行,在multitest\u gt
self.assertTrue(c>“B”,c)
断言者错误:B
----------------------------------------------------------------------
在0.001s内运行了10次测试
失败(失败=3)
您可以立即看到代码生成过程中出现的一些问题。“测试1”来自哪里?我可以将名称更改为较长的“test\u multitest\u gt\u 1”,但接下来哪个测试是1?这里最好从_0开始,而不是从_1开始,在您的例子中,您可能知道这些值可以用作Python函数名

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


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

编写一个测试方法,执行所有测试并捕获所有结果,将自己的诊断消息写入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)            

请注意,当然
my_test()
的名称不能以
test
开头,一种更面向数据的方法可能比在中使用的方法更清晰:

参数化单元测试。 构建单个TestCase类,该类测试其 `somevalue`方法等于数字1到4。 这是通过 创建列表(`cases`) 包含测试规范的词典 然后将列表提供给创建测试用例类的函数。 运行时,输出显示四个案例中有三个失败, 正如所料: >>>导入系统 >>>从unittest导入TextTestRunner >>>运行测试(TextTestRunner(stream=sys.stdout,verbosity=9)) …#doctest:+省略号 测试self.somevalue是否等于4…失败 测试self.somevalue是否等于1…失败 测试self.somevalue是否等于3…失败 测试self.somevalue是否等于2…正常 ====================================================================== 失败:测试self.somevalue是否等于4 ---------------------------------------------------------------------- 回溯(最近一次呼叫最后一次): ... 断言错误:2!=4 ====================================================================== 失败:测试self.somevalue是否等于1 ---------------------------------------------------------------------- 回溯(最近一次呼叫最后一次): ... 断言错误:2!=1 ====================================================================== 失败:测试self.somevalue是否等于3 ---------------------------------------------------------------------- 回溯(最近一次呼叫最后一次): ... 断言错误:2!=3 ---------------------------------------------------------------------- 在…s中运行了4个测试 失败(失败=3) """ 从unittest导入TestCase、TestSuite、defaultTestLoader cases=[{'name':“somevalue_等于_one”, 'doc':“测试self.somevalue是否等于1”, “值”:1}, {'name':“somevalue_等于_tw
"""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))