web-dev-qa-db-ja.com

Python unittest:複数のテストをプログラムで生成しますか?

可能性のある複製:
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テストが実行されます。例を間違って適合させましたか?

47
Nick Heiner

未検証:

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クラスに「固定」されています。

33
doublep

私も同じようなことをしなければなりませんでした。次のように、__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())

これの利点は次のとおりです。

  • 値を追加すると、レポートされるテストの数が増え、より多くのことをしているように感じます。
  • 個々のテストケースは個別に失敗する可能性があります
  • 各入出力値が1つのテストケースに変換されるため、概念的には単純です。
49
Rory

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)

私たちの目標を達成します:

  • 控えめです:通常どおりTestCaseから派生します
  • パラメータ化されたテストを1回だけ書く
  • 各例の値は個別のテストと見なされます
  • デコレータはスタックできるため、例のセットを簡単に使用できます(例:関数を使用して、サンプルファイルまたはディレクトリから値のリストを作成します)
  • ケーキにアイシングすると、署名の任意のアリティで機能します

だからそれはどのように機能しますか?基本的に、デコレータは例を関数の属性に格納します。メタクラスを使用して、装飾されたすべての関数を関数のリストに置き換えます。そして、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
25
Xavier Decoret

鼻( @ Paul Hankin が推奨)

#!/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's one と同様のアプローチ)

#!/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    
11
jfs

Python)でパラメータ化されたテストを実行するために使用できるツールの一部は次のとおりです。

この質問に対するその他の回答については、 質問1676269 も参照してください。

6
akaihola

鼻のテストで、そうです。これを参照してください: https://nose.readthedocs.org/en/latest/writing_tests.html#test-generators

2
user97370

私はロリーのソリューションが最もクリーンで最短だと思います。ただし、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'])
1
johntellsall