How do you generate dynamic (parameterized) unit tests in python?

后端 未结 25 2093
面向向阳花
面向向阳花 2020-11-22 07:09

I have some kind of test data and want to create a unit test for each item. My first idea was to do it like this:

import unittest

l = [[\"foo\", \"a\", \"a\         


        
相关标签:
25条回答
  • 2020-11-22 07:25

    This is called "parametrization".

    There are several tools that support this approach. E.g.:

    • pytest's decorator
    • parameterized

    The resulting code looks like this:

    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)
    

    Which will generate the tests:

    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'
    

    For historical reasons I'll leave the original answer circa 2008 ):

    I use something like this:

    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()
    
    0 讨论(0)
  • 2020-11-22 07:25

    This can be solved elegantly using 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()
    
    0 讨论(0)
  • 2020-11-22 07:26

    Using unittest (since 3.4)

    Since Python 3.4, the standard library unittest package has the subTest context manager.

    See the documentation:

    • 26.4.7. Distinguishing test iterations using subtests
    • subTest

    Example:

    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)
    

    You can also specify a custom message and parameter values to subTest():

    with self.subTest(msg="Checking if p1 equals p2", p1=p1, p2=p2):
    

    Using nose

    The nose testing framework supports this.

    Example (the code below is the entire contents of the file containing the test):

    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
    

    The output of the nosetests command:

    > 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)
    
    0 讨论(0)
  • 2020-11-22 07:26

    Meta-programming is fun, but can get on the way. Most solutions here make it difficult to:

    • selectively launch a test
    • point back to the code given test's name

    So, my first suggestion is to follow the simple/explicit path (works with any test runner):

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

    Since we shouldn't repeat ourselves, my second suggestion builds on @Javier's answer: embrace property based testing. Hypothesis library:

    • is "more relentlessly devious about test case generation than us mere humans"
    • will provide simple count-examples
    • works with any test runner
    • has many more interesting features (statistics, additional test output, ...)

      class TestSequence(unittest.TestCase):

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

    To test your specific examples, just add:

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

    To run only one particular example, you can comment out the other examples (provided example will be run first). You may want to use @given(st.nothing()). Another option is to replace the whole block by:

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

    Ok, you don't have distinct test names. But maybe you just need:

    • a descriptive name of the property under test.
    • which input leads to failure (falsifying example).

    Funnier example

    0 讨论(0)
  • 2020-11-22 07:27
    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)
    

    RESULT:

    >>> 
    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)
    
    0 讨论(0)
  • 2020-11-22 07:27

    Just use metaclasses, as seen here;

    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
    

    Output:

    test_sample (ExampleTestCase) ... OK
    
    0 讨论(0)
提交回复
热议问题