Baz
Baz

Reputation: 13125

Dynamically adding tests to my Test Cases

I wish to add multiple tests to my Test Cases, instead of doing the following:

class TestRestart(unittest.TestCase):

   ...

   def test_modes(self):
      for mode in modes:
         machine.set_mode(mode)
         self.assertTrue(machine.restart())

in many of my tests, I wish to test each mode in its own function. I'm not in a position right now to use libraries like nose. I was thinking I could use the add_tests method below to help me do this:

import unittest

def build_name_1(a):
   return "test " + str(a[0]) + "_" + str(a[1])

def build_name_2(a):
   return "test " + str(a[0]) + "_" + str(a[1]) + "_" + str(a[2])

def add_tests(cls, args, name_builder):
   for a in args:
      def tb(a):
         return lambda self: self.test_body(*a)
      setattr(cls, name_builder(a), tb(a))

class TestEqual(unittest.TestCase):
   def test_body(self, i, j):
      self.assertNotEquals(0, i-j)

args = ((0,0),(1,1))
add_tests(TestEqual, args, build_name_1)

class TestBetween(unittest.TestCase):
   def test_body(self, i, j, k):
      self.assertTrue(i<j<k)

args = ((2,1,2),(2,2,3))
add_tests(TestBetween, args, build_name_2)

if __name__ == '__main__':
   unittest.main()

Is it possible to move the add_tests call to within the TestEqual and TestBetween classes instead of having them outside the classes as I do above? Wouldn't this be better?

What improvements might I make to add_tests? How might I get it to handle named arguments?

Upvotes: 1

Views: 1370

Answers (2)

Blckknght
Blckknght

Reputation: 104682

You could make your add_tests function a class decorator, so that it gets called right at class creation time, rather than separately. If you don't mind deeply nested functions, you can pass in various arguments too.

Here's a version that's equivalent to what you're doing now, just in decorator-factory form:

def add_tests(args, name_builder):
   def decorator(cls):
      for a in args:
          def tb(a):
              return lambda self: self.test_body(*a)
          setattr(cls, name_builder(a), tb(a))
      return cls

You'd call it with:

@add_tests(args, build_name1)
class TestEquals(unittest.TestCase):
    ...

You could probably also handle keyword arguments by taking a sequence of kwargs dicts in addition to the sequence of args tuples. Then you'd just pass them on to the test function, which could be passed as a callable, or if you're dealing with a method, by name (since the class decorator is called before the class is bound to its name). Try this (but note that the name_builder function gets passed an additional argument, so it will need to be updated so it can deal with kwargs too!):

def add_tests(args_sequence, kwargs_sequence, name_builder, test_func=None):
   def decorator(cls):
      if test_func is None:
          test_func = cls.test_body      # fall back on your current behavior
      elif isinstance(test_func, str):
          test_func = getattr(cls, test_func)
      for args, kwargs in zip(args_sequence, kwargs_sequence):
          def tb(a, kw):
              return lambda self: test_func(self, *a, **kw)
          setattr(cls, name_builder(args, kwargs), tb(args, kwargs))
      return cls

You could then do your final examples with:

@add_tests([(mode.SILENT,), (mode.NOISY,)], [{}, {}], build_name_mode, "test_mode_flow")
@add_tests([(1,), ()], [{"speed":33}, {"speed":22, "gear":4}], # can mix args and kwargs
           build_gear_mode, "test_gear_flow")
class TestRestart(unittest.TestCase):
    ...

Upvotes: 2

Jon Poler
Jon Poler

Reputation: 183

import unittest

def outer(i, j):
    """Saves i and j in the inner scope, will be passed an
    instance of Foo on call to inner.
    """
    def inner(inst):
        inst.assertNotEqual(i, j)

    return inner

class Meta(type):
    """Builds test functions before unittest does its discovery.
    """
    def __init__(self, *args, **kwargs):
        for i, j in self.tests:
            setattr(self, self.namebuilder(i, j), outer(i, j))
        super().__init__(*args, **kwargs)        

class Foo(unittest.TestCase, metaclass=Meta):
    def namebuilder(*args):
        return "test_{}_{}".format(*args)

    tests = [(i, j) for i in range(10) for j in range(i+1, 10)]

class Bar(unittest.TestCase, metaclass=Meta):
    def namebuilder(*args):
        return "test_labeled_differently_{}_{}".format(*args)

    tests= [(i, i) for i in range(10)]

if __name__ == '__main__':
    unittest.main()

Here's one way to do it using a meta class. unittest discovers test functions before your class will be instantiated. Think of a class as an instance-builder and a meta class as a class-builder. The meta class will create all of your test functions, so that when the default unittest loader in loader.py https://hg.python.org/cpython/file/322ee2f2e922/Lib/unittest/loader.py calls the function getTestCaseNames, it will find the dynamically created tests. I've included the function from loader.py so you can see for yourself how it works.

def getTestCaseNames(self, testCaseClass):
    """Return a sorted sequence of method names found within testCaseClass
    """
    def isTestMethod(attrname, testCaseClass=testCaseClass,
                     prefix=self.testMethodPrefix):
        return attrname.startswith(prefix) and \
            callable(getattr(testCaseClass, attrname))
    testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
    if self.sortTestMethodsUsing:
        testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
    return testFnNames

Upvotes: 1

Related Questions