Search code examples
pythonpython-3.xdecoratorpython-decoratorsdata-driven-tests

Adding test methods dynamically using decorator


I've been using DDT to parameterize my tests with great success for the past few months. My issue now is that I can't seem to inject a list variable as my data source. Doing so seems to confuse DDT causing it to not parameterize my tests. I started to create my own solution, but I can't seem to figure this last part out.

Here is what I have thus far as decorators -

def data(*values):
    def aaa(func):
        def wrapper(self, *args, **kwargs):
            pass
#             return func(self, *args, **kwargs)
        wrapper.func_name = func.__name__ + 't'
        wrapper.values = values
        return wrapper
    return aaa


def c(cls):
    for name, method in list(cls.__dict__.items()):
        if hasattr(method, 'values'):
            for ticket in method.values[0]:
                test_name = mk_test_name(method.func_name, ticket)
                print(test_name)
                setattr(cls, test_name, method(cls, ticket))
    return cls

And I use it as such -

@c
class IntegrationTests(APITestCase):
    tickets = [1, 2, 3, 4]

    @data(tickets)
    def tes(self, t):
        print(t)

How can I make the Python testing framework recognize that I've added via decorator? I know the methods have been added because issuing the dir command in PDB displays them. The goal for this is that I would duplicate the test(s) I decorate for each item in a list. For those wondering why wrapper() has no code, I did this because uncommenting the line the return call causes the method I decorate to be executed without parameters, thus causing an error.

In my example, I would expect 4 tests with different names to be executed.


Solution

  • The best solution is to use the sub tests feature of unittest in python 3.4. Documentation found here and used like:

    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)
    

    For those who cannot use python 3.4, the following is a poor man's replacement.

    class sub_test_data(object):
    
        def __init__(self, *test_data):
            self.test_data = test_data
    
        def __call__(self, func):
            func.sub_test_data = self.test_data
            func.has_sub_tests = True
            return func
    
    def create_test_driver(func, *args):
        def test_driver(self):
            try:
                func(self, *args)
            except AssertionError as e:
                e.args += ({"test_args": args},)
                raise
        return test_driver  
    
    def create_sub_tests(cls):
        for attr_name, func in list(vars(cls).items()):
            if getattr(func, "has_sub_tests", False):
                for i, value in enumerate(func.sub_test_data):
                    test_name = 'test_{}_subtest{}'.format(attr_name, i)
                    setattr(cls, test_name, create_test_driver(func, value))
        return cls
    
    @create_sub_tests
    class NumbersTest(unittest.TestCase):
        tickets = [0, 1, 2, 3, 4, 5]
    
        @sub_test_data(*tickets)
        def even(self, t):
            self.assertEqual(t % 2, 0)