可能性のある複製:
Pythonで動的(パラメーター化)ユニットテストを生成する方法
テストする関数under_test
と、予想される入力/出力ペアのセットがあります。
[
(2, 332),
(234, 99213),
(9, 3),
# ...
]
これらの入力/出力ペアのそれぞれを、独自のtest_*
メソッドでテストしたいと思います。それは可能ですか?
これは私が望むものですが、すべての単一の入力/出力ペアを単一のテストに強制します:
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()
(また、本当にself.expected_pairs
の定義をsetUp
に入れたいですか?)
UPDATE:doublepのアドバイスを試す:
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テストが実行されます。例を間違って適合させましたか?
未検証:
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.__= 'test_expected_%d' % k
setattr (TestPreReqs, test_method.__name__, test_method)
これを頻繁に使用する場合、ユーティリティ関数やデコレータを使用することでこれを偽装できると思います。この例では、ペアはTestPreReqs
オブジェクトの属性ではないことに注意してください(したがって、setUp
はなくなりました)。むしろ、それらはある意味でTestPreReqs
クラスに「固定」されています。
私も同じようなことをしなければなりませんでした。次のように、__init__
の値を取る単純なTestCase
サブクラスを作成しました。
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)
次に、これらの値を使用してテストスイートを作成しました。
def suite():
suite = unittest.TestSuite()
suite.addTests(KnownGood(input, output) for input, output in known_values)
return suite
その後、メインメソッドから実行できます。
if __== '__main__':
unittest.TextTestRunner().run(suite())
これの利点は次のとおりです。
Pythonの場合と同様に、単純なソリューションを提供するための複雑な方法があります。
その場合、メタプログラミング、デコレーター、およびさまざまな気の利いたPythonトリックを使用して、Nice結果を達成できます。最終的なテストは次のようになります。
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 __== "__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)
私たちの目標を達成します:
だからそれはどのように機能しますか?基本的に、デコレータは例を関数の属性に格納します。メタクラスを使用して、装飾されたすべての関数を関数のリストに置き換えます。そして、unittest.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
誰かがこれをうまくパッケージ化したり、ユニットテスト用のパッチを提案したりしたい場合は、お気軽に!私の名前の引用をいただければ幸いです。
-編集--------
フレームイントロスペクションを使用する準備ができている場合は、コードをはるかに簡単にして、デコレータに完全にカプセル化できます(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.__+ "(" + args_for_parameter + ")"
frame = sys._getframe(1) # pylint: disable-msg=W0212
frame.f_locals[name_for_parameter] = method_for_parameter
return None
return decorator
#!/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)
#!/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.__= "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)でパラメータ化されたテストを実行するために使用できるツールの一部は次のとおりです。
この質問に対するその他の回答については、 質問1676269 も参照してください。
鼻のテストで、そうです。これを参照してください: https://nose.readthedocs.org/en/latest/writing_tests.html#test-generators
私はロリーのソリューションが最もクリーンで最短だと思います。ただし、doublepの「TestCaseでの合成関数の作成」のこのバリエーションも機能します。
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'])