Reputation: 13125
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
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
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