Pythonunit testing:以编程方式生成多个testing?
可能重复:
如何在python中生成dynamic(参数化)的unit testing?
我有一个函数来testing, under_test
,和一套预期的input/输出对:
[ (2, 332), (234, 99213), (9, 3), # ... ]
我希望这些input/输出对中的每一个都在自己的test_*
方法中进行testing。 那可能吗?
这是我想要的东西,但强迫每一个input/输出对进行一个单一的testing:
class TestPreReqs(unittest.TestCase): def setUp(self): self.expected_pairs = [(23, 55), (4, 32)] def test_expected(self): for exp in self.expected_pairs: self.assertEqual(under_test(exp[0]), exp[1]) if __name__ == '__main__': unittest.main()
(另外,我真的想在setUp
中joinself.expected_pairs
定义吗?)
更新:尝试doublep的build议:
class TestPreReqs(unittest.TestCase): def setUp(self): expected_pairs = [ (2, 3), (42, 11), (3, None), (31, 99), ] for k, pair in expected_pairs: setattr(TestPreReqs, 'test_expected_%d' % k, create_test(pair)) def create_test (pair): def do_test_expected(self): self.assertEqual(get_pre_reqs(pair[0]), pair[1]) return do_test_expected if __name__ == '__main__': unittest.main()
这不起作用。 运行0个testing。 我是否错误地修改了这个例子?
未testing:
class TestPreReqs(unittest.TestCase): ... def create_test (pair): def do_test_expected(self): self.assertEqual(under_test(pair[0]), pair[1]) return do_test_expected for k, pair in enumerate ([(23, 55), (4, 32)]): test_method = create_test (pair) test_method.__name__ = 'test_expected_%d' % k setattr (TestPreReqs, test_method.__name__, test_method)
如果你经常使用这个,我猜可以通过使用效用函数和/或装饰器来实现。 请注意,在本例中对不是TestPreReqs
对象的属性(所以setUp
不见了)。 相反,它们在TestPreReqs
类的意义上是“硬连线的”。
我不得不做类似的事情。 我创build了简单的TestCase
子类,它们在__init__
有一个值,如下所示:
class KnownGood(unittest.TestCase): def __init__(self, input, output): super(KnownGood, self).__init__() self.input = input self.output = output def runTest(self): self.assertEqual(function_to_test(self.input), self.output)
然后我用这些值做了一个testing套件:
def suite(): suite = unittest.TestSuite() suite.addTests(KnownGood(input, output) for input, output in known_values) return suite
然后你可以运行你的主要方法:
if __name__ == '__main__': unittest.TextTestRunner().run(suite())
这个的好处是:
- 当你增加更多的价值,报告的testing数量增加,这让你觉得你正在做更多。
- 每个testing用例都可以单独失败
- 这在概念上很简单,因为每个input/输出值都被转换成一个TestCase
像Python一样,提供一个简单的解决scheme有一个复杂的方法。
在这种情况下,我们可以使用元编程,装饰器和各种漂亮的Python技巧来获得不错的结果。 以下是最终testing的样子:
import unittest # some magic code will be added here later class DummyTest(unittest.TestCase): @for_examples(1, 2) @for_examples(3, 4) def test_is_smaller_than_four(self, value): self.assertTrue(value < 4) @for_examples((1,2),(2,4),(3,7)) def test_double_of_X_is_Y(self, x, y): self.assertEqual(2 * x, y) if __name__ == "__main__": unittest.main()
执行这个脚本时,结果是:
..F...F ====================================================================== FAIL: test_double_of_X_is_Y(3,7) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example method(self, *example) File "/Users/xdecoret/Documents/foo.py", line 41, in test_double_of_X_is_Y self.assertEqual(2 * x, y) AssertionError: 6 != 7 ====================================================================== FAIL: test_is_smaller_than_four(4) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/xdecoret/Documents/foo.py", line 22, in method_for_example method(self, *example) File "/Users/xdecoret/Documents/foo.py", line 37, in test_is_smaller_than_four self.assertTrue(value < 4) AssertionError ---------------------------------------------------------------------- Ran 7 tests in 0.001s FAILED (failures=2)
达到我们的目标:
- 它是不显眼的:我们像往常一样从TestCase派生
- 我们只写一次参数化testing
- 每个示例值都被认为是一个单独的testing
- 装饰器可以堆叠,所以很容易使用一组示例(例如,使用函数来构build示例文件或目录中的值列表)
- 锦上添花,它为签名的任意arity起作用
那么它是如何工作的。 基本上,装饰器将示例存储在函数的属性中。 我们使用元类来将每个装饰函数replace为函数列表。 我们用我们的新的魔术代码replaceunittest.TestCase(被粘贴在上面的“魔术”注释中)是:
__examples__ = "__examples__" def for_examples(*examples): def decorator(f, examples=examples): setattr(f, __examples__, getattr(f, __examples__,()) + examples) return f return decorator class TestCaseWithExamplesMetaclass(type): def __new__(meta, name, bases, dict): def tuplify(x): if not isinstance(x, tuple): return (x,) return x for methodname, method in dict.items(): if hasattr(method, __examples__): dict.pop(methodname) examples = getattr(method, __examples__) delattr(method, __examples__) for example in (tuplify(x) for x in examples): def method_for_example(self, method = method, example = example): method(self, *example) methodname_for_example = methodname + "(" + ", ".join(str(v) for v in example) + ")" dict[methodname_for_example] = method_for_example return type.__new__(meta, name, bases, dict) class TestCaseWithExamples(unittest.TestCase): __metaclass__ = TestCaseWithExamplesMetaclass pass unittest.TestCase = TestCaseWithExamples
如果有人想把这个打包好,或者为unittest提供一个补丁,请随意! 我的名字的报价将不胜感激。
– 编辑——–
如果您准备使用帧自省(导入sys模块),代码可以变得更简单并完全封装在装饰器中,
def for_examples(*parameters): def tuplify(x): if not isinstance(x, tuple): return (x,) return x def decorator(method, parameters=parameters): for parameter in (tuplify(x) for x in parameters): def method_for_parameter(self, method=method, parameter=parameter): method(self, *parameter) args_for_parameter = ",".join(repr(v) for v in parameter) name_for_parameter = method.__name__ + "(" + args_for_parameter + ")" frame = sys._getframe(1) # pylint: disable-msg=W0212 frame.f_locals[name_for_parameter] = method_for_parameter return None return decorator
鼻子( @Paul Hankinbuild议)
#!/usr/bin/env python # file: test_pairs_nose.py from nose.tools import eq_ as eq from mymodule import f def test_pairs(): for input, output in [ (2, 332), (234, 99213), (9, 3), ]: yield _test_f, input, output def _test_f(input, output): try: eq(f(input), output) except AssertionError: if input == 9: # expected failure from nose.exc import SkipTest raise SkipTest("expected failure") else: raise if __name__=="__main__": import nose; nose.main()
例:
$ nosetests test_pairs_nose -v test_pairs_nose.test_pairs(2, 332) ... ok test_pairs_nose.test_pairs(234, 99213) ... ok test_pairs_nose.test_pairs(9, 3) ... SKIP: expected failure ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK (SKIP=1)
unittest(类似于@ doublep的方法)
#!/usr/bin/env python import unittest2 as unittest from mymodule import f def add_tests(generator): def class_decorator(cls): """Add tests to `cls` generated by `generator()`.""" for f, input, output in generator(): test = lambda self, i=input, o=output, f=f: f(self, i, o) test.__name__ = "test_%s(%r, %r)" % (f.__name__, input, output) setattr(cls, test.__name__, test) return cls return class_decorator def _test_pairs(): def t(self, input, output): self.assertEqual(f(input), output) for input, output in [ (2, 332), (234, 99213), (9, 3), ]: tt = t if input != 9 else unittest.expectedFailure(t) yield tt, input, output class TestCase(unittest.TestCase): pass TestCase = add_tests(_test_pairs)(TestCase) if __name__=="__main__": unittest.main()
例:
$ python test_pairs_unit2.py -v test_t(2, 332) (__main__.TestCase) ... ok test_t(234, 99213) (__main__.TestCase) ... ok test_t(9, 3) (__main__.TestCase) ... expected failure ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK (expected failures=1)
如果你不想安装unittest2
然后添加:
try: import unittest2 as unittest except ImportError: import unittest if not hasattr(unittest, 'expectedFailure'): import functools def _expectedFailure(func): @functools.wraps(func) def wrapper(*args, **kwargs): try: func(*args, **kwargs) except AssertionError: pass else: raise AssertionError("UnexpectedSuccess") return wrapper unittest.expectedFailure = _expectedFailure
一些可用于在Python中进行参数化testing的工具是:
- 鼻子testing生成器 (仅用于functiontesting,而不是TestCase类)
- 鼻子由David Wolever(也为TestCase类) 参数化
- Boris Feld的unit testing模板
- py.test中的参数化testing
- 奥斯汀宾厄姆参数化testing用例
有关此问题的更多答案,另见问题1676269 。
有了鼻子testing,那么是的。 看到这个: https : //nose.readthedocs.org/en/latest/writing_tests.html#test-generators
我认为罗里的解决scheme是最干净和最短的。 但是,doublep的“在TestCase中创build合成函数”的这种变化也适用于:
from functools import partial class TestAllReports(unittest.TestCase): pass def test_spamreport(name): assert classify(getSample(name))=='spamreport', name for rep in REPORTS: testname = 'test_'+rep testfunc = partial(test_spamreport, rep) testfunc.__doc__ = testname setattr( TestAllReports, testname, testfunc ) if __name__=='__main__': unittest.main(argv=sys.argv + ['--verbose'])