如何在python中生成dynamic(参数化)的unit testing?

我有一些testing数据,并希望为每个项目创build一个unit testing。 我的第一个想法是这样做:

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

这样做的缺点是它可以处理一个testing中的所有数据。 我想为每个项目dynamic生成一个testing。 有什么build议么?

我使用这样的东西:

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

nose-parameterized包可以用来自动化这个过程:

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

哪个会产生testing:

 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/nose_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' 

鼻子testing框架支持这一点 。

示例(下面的代码是包含testing的文件的全部内容):

 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) 

这可以使用Metaclasses来优雅地解决:

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

从Python 3.4开始,为了这个目的,已经引入了unit testing。 详细信息请参阅文档 。 TestCase.subTest是一个上下文pipe理器,它允许在testing中隔离断言,以便使用参数信息报告失败,但不会停止testing的执行。 以下是文档中的示例:

 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) 

testing运行的输出是:

 ====================================================================== 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。

load_tests是2.7中引入的一个鲜为人知的机制,用于dynamic创build一个TestSuite。 有了它,您可以轻松创build参数化testing。

例如:

 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中的所有TestCase。 发现机制不会自动运行其他testing。

或者,也可以使用此票证中显示的inheritance: 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运行你的testing。 那么输出将如下所示:

 =========================== 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-ab] _____________________________ test_me.py:8: in test_me assert left == right, name E AssertionError: bar ==================== 1 failed, 2 passed in 0.01 seconds ==================== 

这很简单! 另外pytest有更多的function,如fixturesmarkassert等…

使用ddt库。 它为testing方法添加了简单的装饰器:

 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 ,并且与标准库unit testing模块一起工作得很好。

您将从尝试TestScenarios库中受益。

testscenarios为Pythonunit testing样式testing提供干净的dependency injection。 这可以用于接口testing(通过单个testing套件testing许多实现)或传统的dependency injection(在testing代码本身外部提供依赖关系的testing,允许在不同情况下进行简单的testing)。

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

与现有testing集成非常容易,只需要很less的更改(如果有的话)。 它也支持鼻子多重处理插件。

不是你也可以有一个自定义setupfunction每个testing。

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

也可以像使用内置插件attrib一样传递nosetest参数,这样您只能使用特定的参数运行特定的testing:

 nosetest -a number=2 

还有一个假设,它添加了基于模糊或属性的testing: https : //pypi.python.org/pypi/hypothesis

这是一个非常强大的testing方法。

那天我看到ParamUnittest的时候,看到了radon的源代码( github repo上的示例用法 )。 它应该与扩展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) 

我使用元类和装饰器来生成testing。 你可以检查我的实现python_wrap_cases 。 这个库不需要任何testing框架。

你的例子:

 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生成所有可能的testing组合

 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 

只要使用元类,如这里所见;

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

我有一个非常特殊的参数化testing风格的麻烦。 我们所有的Seleniumtesting都可以在本地运行,但是它们也应该能够在SauceLab上的多个平台上远程运行。 基本上,我想要大量的已经写好的testing用例,并用尽可能less的代码进行参数化来编码。 此外,我需要能够将parameter passing给setUp方法,这在其他地方我还没有看到任何解决scheme。

以下是我想到的:

 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 

有了这个,我所要做的就是将一个简单的装饰器@sauce_labs()添加到每个常规的旧TestCase中,现在运行它们时,它们被封装起来并被重写,所有的testing方法都被参数化和重命名。 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初始化。

无论如何,我希望这可能有助于有人想要做一个类似的“全球”参数化的testing!

这个解决scheme适用于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() 

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

基于元类的答案仍然可以在Python3中使用,但是不用__metaclass__属性,而必须使用metaclass参数,如下所示:

 class ExampleTestCase(TestCase,metaclass=DocTestMeta): pass 

元编程是有趣的,但可以开始。 这里的大多数解决scheme都很难:

  • select性地开展testing
  • 指向给定testing名称的代码

所以,我的第一个build议是遵循简单/明确的path(与任何testing运行者一起工作):

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

由于我们不应该重复自己,我的第二个build议build立在@ Javier的答案上:拥抱基于属性的testing。 假设库:

  • “对于testing用例的生成比我们仅仅是人类更加无情地狡猾”
  • 将提供简单的计数示例
  • 与任何testing跑步者合作
  • 有更多有趣的function(统计,额外的testing输出…)

    class TestSequence(unittest.TestCase):

     @given(st.text(), st.text()) def test_complex_property(self, a, b): self.assertEqual(a,b) 

要testing您的具体示例,只需添加:

  @example("a", "a") @example("a", "b") @example("b", "b") 

为了只运行一个特定的例子,你可以注释掉其他的例子(例子会先运行)。 你可能想使用@given(st.nothing()) 。 另一种select是通过以下方式replace整个块:

  @given(st.just("a"), st.just("b")) 

好吧,你没有明确的testing名称。 但也许你只需要:

  • 被测财产的描述性名称。
  • 哪个input导致失败(伪造示例)。

有趣的例子

除了使用setattr,我们可以使用python 3.2以来的load_tests。 请参阅blog 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() 

以下是我的解决scheme。 我发现这有用的时候:1.应unittest.Testcase和unit testing发现工作2.有一套testing运行不同的参数设置。 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