如何在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()

这样做的缺点是它在一个测试中处理所有数据。我想在飞行中为每个项目生成一个测试。有什么建议吗?

179322 次浏览

这被称为“参数化”。

有几个工具支持这种方法。例如:

结果代码如下所示:

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

使用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命令输出信息如下:

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

你会从尝试TestScenarios库中受益。

testscenes为python unittest风格的测试提供了干净的依赖注入。这可以用于接口测试(通过单个测试套件测试许多实现)或经典的依赖项注入(在测试代码本身的外部提供依赖项测试,允许在不同情况下轻松测试)。

这可以使用元类优雅地解决:

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

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返回的测试套件中的所有测试用例。发现机制不会自动运行其他测试。

或者,您也可以使用继承,如此票据所示:http://bugs.python.org/msg151444

这可以通过使用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

并使用命令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有更多的功能,如fixturesmarkassert等。

你可以使用nose-ittr插件(pip install nose-ittr)。

它非常容易与现有的测试集成,并且只需要极小的更改(如果有的话)。它还支持鼻子多处理插件。

注意,每个测试也可以自定义setup函数。

@ittr(number=[1, 2, 3, 4])
def test_even(self):
assert_equal(self.number % 2, 0)

也可以像内置插件attrib一样传递nosetest参数。通过这种方式,你可以只运行特定参数的特定测试:

nosetest -a number=2

前几天我在查看 (在GitHub存储库上的示例使用)的源代码时遇到了< >强ParamUnittest < / >强。它应该与扩展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)

自Python 3.4起,子测试已为此目的引入到unittest。详见的文档。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。

使用滴滴涕库。它为测试方法添加了简单的装饰器:

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模块一起出色地工作。

只使用元类,如这里所示;

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

我使用元类和装饰器来生成测试。你可以检查我的实现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__listb__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

还有假设,它添加了模糊或基于属性的测试。

这是一种非常强大的测试方法。

你可以使用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)

我在一种非常特殊的参数化测试风格上遇到了麻烦。我们所有的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(),现在当运行它们时,它们被打包并重写,这样所有的测试方法都被参数化并重命名。logngs .test_login(self)以logngs .test_login_internet_explorer_10.0(self)、logngs .test_login_internet_explorer_11.0(self)和logngs .test_login_firefox_43.0(self)的形式运行,每一个都有参数self。平台来决定在什么浏览器/平台上运行,甚至在LoginTests中。setUp,这对我的任务至关重要,因为到SauceLabs的连接就是在这里初始化的。

无论如何,我希望这对那些希望对他们的测试进行类似的“全局”参数化的人有所帮助!

除了使用setattr,我们还可以在Python 3.2及更高版本中使用load_tests

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

以下是我的解决方案。我发现这个方法很有用:

  1. 应该适用于unittest。Testcase和unittest发现

  2. 为不同的参数设置运行一组测试。

  3. 非常简单,不依赖于其他包

     import 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
    

此解决方案适用于Python 2和Python 3的unittestnose:

#!/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()
import unittest


def generator(test_class, a, b):
def test(self):
self.assertEqual(a, b)
return test


def add_test_methods(test_class):
# The 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)

基于元类的答案在Python 3中仍然有效,但必须使用__metaclass__属性,而不是使用metaclass参数,例如:

class ExampleTestCase(TestCase,metaclass=DocTestMeta):
pass

元编程很有趣,但它也会碍事。这里的大多数解决方案都很难:

  • 有选择地启动测试
  • 指向给出测试名称的代码

所以,我的第一个建议是遵循简单/显式路径(适用于任何测试运行程序):

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

既然我们不应该重复,我的第二个建议建立在哈维尔的回答之上:接受基于属性的测试。假设库:

  • “在生成测试用例方面比我们人类更加无情地迂回”;

  • 将提供简单的计数示例

  • 适用于任何测试运行程序

  • 有许多更有趣的特性(统计信息、额外的测试输出,…)

    类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"))

好的,您没有不同的测试名称。但也许你只需要:

  • 被测属性的描述性名称。
  • 哪个输入会导致失败(伪造的例子)。

< a href = " http://hypothesis。works/articles/how-not-to-die-hard with-hypothesis" rel="nofollow noreferrer">更有趣的例子

我有麻烦使这些工作setUpClass

下面是哈维尔的回答的一个版本,它允许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

我发现这很适合我的目的,特别是当我需要生成在数据集合上执行稍微不同的过程的测试时。

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接口的实现。

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)

这实际上与前面回答中提到的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, arg1, arg2):
...