问题:如何在python中生成动态(参数化)单元测试?
我有某种测试数据,并且想为每个项目创建一个单元测试。我的第一个想法是这样做:
import unittest
l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]
class TestSequence(unittest.TestCase):
def testsample(self):
for name, a,b in l:
print "test", name
self.assertEqual(a,b)
if __name__ == '__main__':
unittest.main()
这样做的缺点是它可以在一次测试中处理所有数据。我想即时为每个项目生成一个测试。有什么建议?
回答 0
这称为“参数化”。
有几种工具支持这种方法。例如:
产生的代码如下所示:
from parameterized import parameterized
class TestSequence(unittest.TestCase):
@parameterized.expand([
["foo", "a", "a",],
["bar", "a", "b"],
["lee", "b", "b"],
])
def test_sequence(self, name, a, b):
self.assertEqual(a,b)
将会生成测试:
test_sequence_0_foo (__main__.TestSequence) ... ok
test_sequence_1_bar (__main__.TestSequence) ... FAIL
test_sequence_2_lee (__main__.TestSequence) ... ok
======================================================================
FAIL: test_sequence_1_bar (__main__.TestSequence)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/parameterized/parameterized.py", line 233, in <lambda>
standalone_func = lambda *a: func(*(a + p.args), **p.kwargs)
File "x.py", line 12, in test_sequence
self.assertEqual(a,b)
AssertionError: 'a' != 'b'
由于历史原因,我会在2008年左右保留原始答案):
我用这样的东西:
import unittest
l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]
class TestSequense(unittest.TestCase):
pass
def test_generator(a, b):
def test(self):
self.assertEqual(a,b)
return test
if __name__ == '__main__':
for t in l:
test_name = 'test_%s' % t[0]
test = test_generator(t[1], t[2])
setattr(TestSequense, test_name, test)
unittest.main()
回答 1
使用unittest(从3.4开始)
从Python 3.4开始,标准库unittest
包具有subTest
上下文管理器。
请参阅文档:
例:
from unittest import TestCase
param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]
class TestDemonstrateSubtest(TestCase):
def test_works_as_expected(self):
for p1, p2 in param_list:
with self.subTest():
self.assertEqual(p1, p2)
您还可以将自定义消息和参数值指定为subTest()
:
with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):
用鼻子
示例(下面的代码是包含测试的文件的全部内容):
param_list = [('a', 'a'), ('a', 'b'), ('b', 'b')]
def test_generator():
for params in param_list:
yield check_em, params[0], params[1]
def check_em(a, b):
assert a == b
鼻子测试命令的输出:
> nosetests -v
testgen.test_generator('a', 'a') ... ok
testgen.test_generator('a', 'b') ... FAIL
testgen.test_generator('b', 'b') ... ok
======================================================================
FAIL: testgen.test_generator('a', 'b')
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.5/site-packages/nose-0.10.1-py2.5.egg/nose/case.py", line 203, in runTest
self.test(*self.arg)
File "testgen.py", line 7, in check_em
assert a == b
AssertionError
----------------------------------------------------------------------
Ran 3 tests in 0.006s
FAILED (failures=1)
回答 2
这可以使用元类优雅地解决:
import unittest
l = [["foo", "a", "a",], ["bar", "a", "b"], ["lee", "b", "b"]]
class TestSequenceMeta(type):
def __new__(mcs, name, bases, dict):
def gen_test(a, b):
def test(self):
self.assertEqual(a, b)
return test
for tname, a, b in l:
test_name = "test_%s" % tname
dict[test_name] = gen_test(a,b)
return type.__new__(mcs, name, bases, dict)
class TestSequence(unittest.TestCase):
__metaclass__ = TestSequenceMeta
if __name__ == '__main__':
unittest.main()
回答 3
从Python 3.4开始,为此引入了子测试来进行单元测试。有关详细信息,请参见文档。TestCase.subTest是一个上下文管理器,它允许隔离测试中的断言,以便将失败与参数信息一起报告,但不会停止测试执行。这是文档中的示例:
class NumbersTest(unittest.TestCase):
def test_even(self):
"""
Test that numbers between 0 and 5 are all even.
"""
for i in range(0, 6):
with self.subTest(i=i):
self.assertEqual(i % 2, 0)
测试运行的输出为:
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
======================================================================
FAIL: test_even (__main__.NumbersTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
File "subtests.py", line 32, in test_even
self.assertEqual(i % 2, 0)
AssertionError: 1 != 0
这也是unittest2的一部分,因此可用于早期版本的Python。
回答 4
load_tests是2.7中引入的鲜为人知的机制,用于动态创建TestSuite。有了它,您可以轻松创建参数化测试。
例如:
import unittest
class GeneralTestCase(unittest.TestCase):
def __init__(self, methodName, param1=None, param2=None):
super(GeneralTestCase, self).__init__(methodName)
self.param1 = param1
self.param2 = param2
def runTest(self):
pass # Test that depends on param 1 and 2.
def load_tests(loader, tests, pattern):
test_cases = unittest.TestSuite()
for p1, p2 in [(1, 2), (3, 4)]:
test_cases.addTest(GeneralTestCase('runTest', p1, p2))
return test_cases
该代码将运行load_tests返回的TestSuite中的所有TestCases。发现机制不会自动运行其他测试。
另外,您也可以使用此票证中所示的继承:http : //bugs.python.org/msg151444
回答 5
可以通过使用pytest完成。只需将test_me.py
内容写入文件即可:
import pytest
@pytest.mark.parametrize('name, left, right', [['foo', 'a', 'a'],
['bar', 'a', 'b'],
['baz', 'b', 'b']])
def test_me(name, left, right):
assert left == right, name
然后使用command运行测试py.test --tb=short test_me.py
。然后输出将如下所示:
=========================== test session starts ============================
platform darwin -- Python 2.7.6 -- py-1.4.23 -- pytest-2.6.1
collected 3 items
test_me.py .F.
================================= FAILURES =================================
_____________________________ test_me[bar-a-b] _____________________________
test_me.py:8: in test_me
assert left == right, name
E AssertionError: bar
==================== 1 failed, 2 passed in 0.01 seconds ====================
很简单!此外pytest具有更多的功能,如fixtures
,mark
,assert
,等…
回答 6
使用ddt库。它为测试方法添加了简单的装饰器:
import unittest
from ddt import ddt, data
from mycode import larger_than_two
@ddt
class FooTestCase(unittest.TestCase):
@data(3, 4, 12, 23)
def test_larger_than_two(self, value):
self.assertTrue(larger_than_two(value))
@data(1, -3, 2, 0)
def test_not_larger_than_two(self, value):
self.assertFalse(larger_than_two(value))
可以使用安装该库pip
。它不需要nose
,并且可以与标准库unittest
模块一起很好地工作。
回答 7
您可以从TestScenarios库中受益。
testscenarios为python unittest样式测试提供了干净的依赖注入。它可以用于接口测试(通过单个测试套件测试许多实现)或经典的依赖注入(为测试代码本身提供带有外部依赖的测试,从而允许在不同情况下轻松进行测试)。
回答 8
还有一个假设添加了模糊测试或基于属性的测试:https : //pypi.python.org/pypi/hypothesis
这是一种非常强大的测试方法。
回答 9
您可以使用鼻子-ittr插件(pip install nose-ittr
)。
与现有测试集成非常容易,只需进行最小的更改(如果有)。它还支持鼻子多处理插件。
并非setup
每个测试都可以具有自定义功能。
@ittr(number=[1, 2, 3, 4])
def test_even(self):
assert_equal(self.number % 2, 0)
也可以nosetest
像使用其内置插件一样传递参数attrib
,这样您就可以只运行带有特定参数的特定测试:
nosetest -a number=2
回答 10
我使用元类和装饰器来生成测试。您可以检查我的实现python_wrap_cases。该库不需要任何测试框架。
你的例子:
import unittest
from python_wrap_cases import wrap_case
@wrap_case
class TestSequence(unittest.TestCase):
@wrap_case("foo", "a", "a")
@wrap_case("bar", "a", "b")
@wrap_case("lee", "b", "b")
def testsample(self, name, a, b):
print "test", name
self.assertEqual(a, b)
控制台输出:
testsample_u'bar'_u'a'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test bar
FAIL
testsample_u'foo'_u'a'_u'a' (tests.example.test_stackoverflow.TestSequence) ... test foo
ok
testsample_u'lee'_u'b'_u'b' (tests.example.test_stackoverflow.TestSequence) ... test lee
ok
您也可以使用生成器。例如,此代码生成带有参数a__list
和的测试的所有可能组合b__list
import unittest
from python_wrap_cases import wrap_case
@wrap_case
class TestSequence(unittest.TestCase):
@wrap_case(a__list=["a", "b"], b__list=["a", "b"])
def testsample(self, a, b):
self.assertEqual(a, b)
控制台输出:
testsample_a(u'a')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... ok
testsample_a(u'a')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'a') (tests.example.test_stackoverflow.TestSequence) ... FAIL
testsample_a(u'b')_b(u'b') (tests.example.test_stackoverflow.TestSequence) ... ok
回答 11
我碰上了ParamUnittest看源代码,当有一天,氡(在github上回购用法示例)。它应与其他扩展TestCase的框架(如Nose)一起使用。
这是一个例子:
import unittest
import paramunittest
@paramunittest.parametrized(
('1', '2'),
#(4, 3), <---- uncomment to have a failing test
('2', '3'),
(('4', ), {'b': '5'}),
((), {'a': 5, 'b': 6}),
{'a': 5, 'b': 6},
)
class TestBar(TestCase):
def setParameters(self, a, b):
self.a = a
self.b = b
def testLess(self):
self.assertLess(self.a, self.b)
回答 12
import unittest
def generator(test_class, a, b):
def test(self):
self.assertEqual(a, b)
return test
def add_test_methods(test_class):
#First element of list is variable "a", then variable "b", then name of test case that will be used as suffix.
test_list = [[2,3, 'one'], [5,5, 'two'], [0,0, 'three']]
for case in test_list:
test = generator(test_class, case[0], case[1])
setattr(test_class, "test_%s" % case[2], test)
class TestAuto(unittest.TestCase):
def setUp(self):
print 'Setup'
pass
def tearDown(self):
print 'TearDown'
pass
_add_test_methods(TestAuto) # It's better to start with underscore so it is not detected as a test itself
if __name__ == '__main__':
unittest.main(verbosity=1)
结果:
>>>
Setup
FTearDown
Setup
TearDown
.Setup
TearDown
.
======================================================================
FAIL: test_one (__main__.TestAuto)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:/inchowar/Desktop/PyTrash/test_auto_3.py", line 5, in test
self.assertEqual(a, b)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 3 tests in 0.019s
FAILED (failures=1)
回答 13
只需使用元类,如此处所示;
class DocTestMeta(type):
"""
Test functions are generated in metaclass due to the way some
test loaders work. For example, setupClass() won't get called
unless there are other existing test methods, and will also
prevent unit test loader logic being called before the test
methods have been defined.
"""
def __init__(self, name, bases, attrs):
super(DocTestMeta, self).__init__(name, bases, attrs)
def __new__(cls, name, bases, attrs):
def func(self):
"""Inner test method goes here"""
self.assertTrue(1)
func.__name__ = 'test_sample'
attrs[func.__name__] = func
return super(DocTestMeta, cls).__new__(cls, name, bases, attrs)
class ExampleTestCase(TestCase):
"""Our example test case, with no methods defined"""
__metaclass__ = DocTestMeta
输出:
test_sample (ExampleTestCase) ... OK
回答 14
您可以使用TestSuite
和自定义TestCase
类。
import unittest
class CustomTest(unittest.TestCase):
def __init__(self, name, a, b):
super().__init__()
self.name = name
self.a = a
self.b = b
def runTest(self):
print("test", self.name)
self.assertEqual(self.a, self.b)
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTest(CustomTest("Foo", 1337, 1337))
suite.addTest(CustomTest("Bar", 0xDEAD, 0xC0DE))
unittest.TextTestRunner().run(suite)
回答 15
我发现这很适合我的目的,尤其是当我需要生成对数据集合的过程做些微改动的测试时。
import unittest
def rename(newName):
def renamingFunc(func):
func.__name__ == newName
return func
return renamingFunc
class TestGenerator(unittest.TestCase):
TEST_DATA = {}
@classmethod
def generateTests(cls):
for dataName, dataValue in TestGenerator.TEST_DATA:
for func in cls.getTests(dataName, dataValue):
setattr(cls, "test_{:s}_{:s}".format(func.__name__, dataName), func)
@classmethod
def getTests(cls):
raise(NotImplementedError("This must be implemented"))
class TestCluster(TestGenerator):
TEST_CASES = []
@staticmethod
def getTests(dataName, dataValue):
def makeTest(case):
@rename("{:s}".format(case["name"]))
def test(self):
# Do things with self, case, data
pass
return test
return [makeTest(c) for c in TestCluster.TEST_CASES]
TestCluster.generateTests()
的TestGenerator
类可用于产卵不同组的测试案例等TestCluster
。
TestCluster
可以认为是TestGenerator
接口的实现。
回答 16
该解决方案可与unittest
和nose
为Python 2和Python 3:
#!/usr/bin/env python
import unittest
def make_function(description, a, b):
def ghost(self):
self.assertEqual(a, b, description)
print(description)
ghost.__name__ = 'test_{0}'.format(description)
return ghost
class TestsContainer(unittest.TestCase):
pass
testsmap = {
'foo': [1, 1],
'bar': [1, 2],
'baz': [5, 5]}
def generator():
for name, params in testsmap.iteritems():
test_func = make_function(name, params[0], params[1])
setattr(TestsContainer, 'test_{0}'.format(name), test_func)
generator()
if __name__ == '__main__':
unittest.main()
回答 17
我一直在遇到非常特殊的参数化测试样式。我们所有的Selenium测试都可以在本地运行,但是它们也应该能够在SauceLabs上的多个平台上远程运行。基本上,我想使用大量已经编写的测试用例,并以尽可能少的代码更改对它们进行参数化。此外,我需要能够将参数传递到setUp方法中,这在其他地方还没有任何解决方案。
这是我想出的:
import inspect
import types
test_platforms = [
{'browserName': "internet explorer", 'platform': "Windows 7", 'version': "10.0"},
{'browserName': "internet explorer", 'platform': "Windows 7", 'version': "11.0"},
{'browserName': "firefox", 'platform': "Linux", 'version': "43.0"},
]
def sauce_labs():
def wrapper(cls):
return test_on_platforms(cls)
return wrapper
def test_on_platforms(base_class):
for name, function in inspect.getmembers(base_class, inspect.isfunction):
if name.startswith('test_'):
for platform in test_platforms:
new_name = '_'.join(list([name, ''.join(platform['browserName'].title().split()), platform['version']]))
new_function = types.FunctionType(function.__code__, function.__globals__, new_name,
function.__defaults__, function.__closure__)
setattr(new_function, 'platform', platform)
setattr(base_class, new_name, new_function)
delattr(base_class, name)
return base_class
这样,我要做的就是在每个常规的旧TestCase上添加一个简单的装饰器@sauce_labs(),现在在运行它们时,将它们包装并重写,以便对所有测试方法进行参数化和重命名。LoginTests.test_login(self)以LoginTests.test_login_internet_explorer_10.0(self),LoginTests.test_login_internet_explorer_11.0(self)和LoginTests.test_login_firefox_43.0(self)运行,并且每个参数都有参数self.platform来决定使用哪种浏览器/即使在LoginTests.setUp中也可以运行的平台,这对我的任务至关重要,因为这是初始化与SauceLabs的连接的地方。
无论如何,我希望这对希望对测试进行类似“全局”参数化的人有所帮助!
回答 18
基于元类的答案在Python3中仍然有效,但是__metaclass__
必须使用metaclass
参数来代替属性,如下所示:
class ExampleTestCase(TestCase,metaclass=DocTestMeta):
pass
回答 19
元编程很有趣,但是可以继续前进。这里的大多数解决方案都很难:
- 有选择地启动测试
- 指向指定测试名称的代码
因此,我的第一个建议是遵循简单/明确的路径(适用于任何测试运行程序):
import unittest
class TestSequence(unittest.TestCase):
def _test_complex_property(self, a, b):
self.assertEqual(a,b)
def test_foo(self):
self._test_complex_property("a", "a")
def test_bar(self):
self._test_complex_property("a", "b")
def test_lee(self):
self._test_complex_property("b", "b")
if __name__ == '__main__':
unittest.main()
因为我们不应该重复自己,所以我的第二个建议建立在@Javier的答案上:接受基于属性的测试。假设库:
- “对于测试用例的生成要比对我们单纯的人类更加不懈地弯曲”
- 将提供简单的计数示例
- 与任何测试跑步者一起使用
具有更多有趣的功能(统计信息,附加测试输出等)
类TestSequence(unittest.TestCase):
@given(st.text(), st.text()) def test_complex_property(self, a, b): self.assertEqual(a,b)
要测试您的特定示例,只需添加:
@example("a", "a")
@example("a", "b")
@example("b", "b")
要仅运行一个特定示例,您可以注释掉其他示例(首先运行提供的示例)。您可能要使用@given(st.nothing())
。另一种选择是将整个块替换为:
@given(st.just("a"), st.just("b"))
好的,您没有不同的测试名称。但也许您只需要:
- 被测属性的描述性名称。
- 哪个输入会导致失败(伪造的示例)。
回答 20
超级晚会晚了,但我很难为这些工作setUpClass
。
这是@Javier答案的一个版本,可以setUpClass
访问动态分配的属性。
import unittest
class GeneralTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
print ''
print cls.p1
print cls.p2
def runTest1(self):
self.assertTrue((self.p2 - self.p1) == 1)
def runTest2(self):
self.assertFalse((self.p2 - self.p1) == 2)
def load_tests(loader, tests, pattern):
test_cases = unittest.TestSuite()
for p1, p2 in [(1, 2), (3, 4)]:
clsname = 'TestCase_{}_{}'.format(p1, p2)
dct = {
'p1': p1,
'p2': p2,
}
cls = type(clsname, (GeneralTestCase,), dct)
test_cases.addTest(cls('runTest1'))
test_cases.addTest(cls('runTest2'))
return test_cases
产出
1
2
..
3
4
..
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
回答 21
只是在混合中提出另一种解决方案;)
这实际上与上述内容相同parameterized
,但特定于unittest
:
def sub_test(param_list):
"""Decorates a test case to run it as a set of subtests."""
def decorator(f):
@functools.wraps(f)
def wrapped(self):
for param in param_list:
with self.subTest(**param):
f(self, **param)
return wrapped
return decorator
用法示例:
class TestStuff(unittest.TestCase):
@sub_test([
dict(arg1='a', arg2='b'),
dict(arg1='x', arg2='y'),
])
def test_stuff(self, a, b):
...
回答 22
除了使用setattr之外,我们还可以从python 3.2开始使用load_tests。请参阅博客文章blog.livreuro.com/en/coding/python/how-to-generate-discoverable-unit-tests-in-python-dynamically/
class Test(unittest.TestCase):
pass
def _test(self, file_name):
open(file_name, 'r') as f:
self.assertEqual('test result',f.read())
def _generate_test(file_name):
def test(self):
_test(self, file_name)
return test
def _generate_tests():
for file in files:
file_name = os.path.splitext(os.path.basename(file))[0]
setattr(Test, 'test_%s' % file_name, _generate_test(file))
test_cases = (Test,)
def load_tests(loader, tests, pattern):
_generate_tests()
suite = TestSuite()
for test_class in test_cases:
tests = loader.loadTestsFromTestCase(test_class)
suite.addTests(tests)
return suite
if __name__ == '__main__':
_generate_tests()
unittest.main()
回答 23
以下是我的解决方案。在以下情况下,我认为这很有用:1.应该适用于unittest.Testcase和unittest discover。2.为不同的参数设置运行一组测试。3.非常简单,不依赖其他包导入unittest
class BaseClass(unittest.TestCase):
def setUp(self):
self.param = 2
self.base = 2
def test_me(self):
self.assertGreaterEqual(5, self.param+self.base)
def test_me_too(self):
self.assertLessEqual(3, self.param+self.base)
class Child_One(BaseClass):
def setUp(self):
BaseClass.setUp(self)
self.param = 4
class Child_Two(BaseClass):
def setUp(self):
BaseClass.setUp(self)
self.param = 1
回答 24
import unittest
def generator(test_class, a, b,c,d,name):
def test(self):
print('Testexecution=',name)
print('a=',a)
print('b=',b)
print('c=',c)
print('d=',d)
return test
def add_test_methods(test_class):
test_list = [[3,3,5,6, 'one'], [5,5,8,9, 'two'], [0,0,5,6, 'three'],[0,0,2,3,'Four']]
for case in test_list:
print('case=',case[0], case[1],case[2],case[3],case[4])
test = generator(test_class, case[0], case[1],case[2],case[3],case[4])
setattr(test_class, "test_%s" % case[4], test)
class TestAuto(unittest.TestCase):
def setUp(self):
print ('Setup')
pass
def tearDown(self):
print ('TearDown')
pass
add_test_methods(TestAuto)
if __name__ == '__main__':
unittest.main(verbosity=1)