编写可重用的(参数化)unittest.TestCase 方法
可能重复的问题:
如何在Python中生成动态(参数化)单元测试?
我正在使用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用不同的输入来尝试这个测试?
6 个回答
我想你想要的功能是“参数化测试”。
我觉得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模块中,像这样的简单装饰器是无法“重复”测试多次的,但我认为可以通过结合使用装饰器和 metaclass 来实现这个功能(metaclass 应该观察所有以'test*'开头的方法,并在不同的自动生成名称下重复那些应用了装饰器的方法)。
如果你真的想要有多个单元测试,那就需要多个方法。实现这一点的唯一方法是通过某种代码生成。你可以通过元类来实现,或者在定义后调整类,包括(如果你使用的是Python 2.6)通过类装饰器。
这里有一个解决方案,它会寻找特殊的'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()
这是我运行它时的输出结果
% 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)
你可以立刻看到代码生成带来的一些问题。“test_gt_1”是从哪里来的?我可以把名字改成更长的“test_multitest_gt_1”,但这样的话,1代表哪个测试呢?这里更好的做法是从_0开始,而不是_1,也许在你的情况下,你知道这些值可以用作Python函数名。
我不喜欢这种方法。我曾经在一些代码库上工作过,它们自动生成测试方法(在一个案例中使用了元类),我发现这比实际有用的要难理解得多。当一个测试失败时,很难找出失败的原因,而且很难插入调试代码来探查失败的原因。
(在我这里写的例子中,调试失败并没有那么难,远没有我之前遇到的那个特定元类的方法那么复杂。)
在Python中,有一些工具可以用来进行参数化测试,下面是一些常见的:
- Nose测试生成器(只适用于函数测试,不适用于TestCase类)
- nose-parametrized,由David Wolever开发(也适用于TestCase类)
- Unittest模板,由Boris Feld提供
- py.test中的参数化测试
- parametrized-testcase,由Austin Bingham开发
- DDT(数据驱动测试),由Carles Barrobés提供,适用于单元测试