decorators to add test* TestCase methods

B

Bruce Cropley

Hi all

I'm trying to generate test methods in a unittest TestCase
subclass, using decorators. I'd like to be able to say:

class MyTestCase(unittest.TestCase):
@genTests(["Buy", "Sell"], [1,2,3], [True, False])
def something(self, side, price, someFlag):
# etc...

And have it generate functions called:
test_Buy_1_True_something
test_Buy_1_False_something
through to...
test_Sell_3_False_something

Each of these would call the something function with the
expected parameters. I guess that the new 2.5 partial()
could help with this.

There are two problems:
- Decorators don't seem to be able to add functions to the
class because it is still being defined. I know decorators
aren't really intended for this, but it would be nice to keep
the expansion call near the generic test method source.
- There seems to be a bug with creating functions in a loop,
see below...

Here's my attempt so far:

---------8<-----------
#!/usr/bin/env python

# Generate all combinations of values of args.
def product(*args):
if len(args) == 0:
return [[]]
return [[val] + rest
for val in args[0]
for rest in product(*args[1:])]

class Test:
# Making this a classmethod doesn't seem to have gained me much.
# perhaps it should be just a function, and use func.im_class?
@classmethod
def genTests(cls, func, *args):
""" Generate all test functions of the form:
test_B_Harry_{func.func_name}
test_B_Sally_{func.func_name}
test_S_Harry_{func.func_name}
test_S_Sally_{func.func_name}
... which call self.func with the corresponding parameters.
"""
for param_combo in product(*args):
#print "In loop, generating %s" % param_combo
def testFunc(self):
#print "In test func: %s" % param_combo
func(self, *param_combo)
func_name = "_".join(["test"] + param_combo +
[func.func_name])
setattr(cls, "%s" % func_name, testFunc)

class Derived(Test):
# I want to be able to do:
# @genTests(["B", "S"], ["Harry", "Sally"])
def blah(self, side, name):
print "blah: %s %s" % (side, name)

t = Derived()

# Hack
Derived.genTests(Derived.blah, ["B", "S"], ["Harry", "Sally"])

t.test_S_Sally_blah()

# This is failing (generates test_S_Sally_blah),
# and appears to be a python bug:
t.test_B_Harry_blah()
---------8<----------

Does anyone have any suggestions?

Thanks,
Bruce
 
D

Duncan Booth

Bruce said:
Hi all

I'm trying to generate test methods in a unittest TestCase
subclass, using decorators. I'd like to be able to say:

class MyTestCase(unittest.TestCase):
@genTests(["Buy", "Sell"], [1,2,3], [True, False])
def something(self, side, price, someFlag):
# etc...
I would make the decorator simply save its arguments as attributes on the
function and then add an explicit call after the class has been created to
go and generate the tests.

so (untested code):

def generateTests(klass):
for name in dir(klass):
fn = getattr(klass, name)
if hasattr(fn, 'genTests'):
# generate tests here...

def genTests(*args):
def decorator(f):
f.genTests = args
return f
return decorator

and then:

class MyTestCase(unittest.TestCase):
@genTests(["Buy", "Sell"], [1,2,3], [True, False])
def something(self, side, price, someFlag):
# etc...

generateTests(MyTestCase)

You could automate the call to generateTests using a metaclass if you
wanted to avoid the explicit call.
 
P

Peter Otten

Bruce said:
I'm trying to generate test methods in a unittest TestCase
subclass, using decorators. I'd like to be able to say:

class MyTestCase(unittest.TestCase):
@genTests(["Buy", "Sell"], [1,2,3], [True, False])
def something(self, side, price, someFlag):
# etc...

And have it generate functions called:
test_Buy_1_True_something
test_Buy_1_False_something
through to...
test_Sell_3_False_something

I think that's a nice idea. Here is a preliminary implementation:

#!/usr/bin/env python
import unittest

def combine(axes):
if axes:
for value in axes[0]:
for rest in combine(axes[1:]):
yield (value,) + rest
else:
yield ()

def make_method(func, name, args):
def method(self):
return func(self, *args)
method.__name__ = name
return method

class TestGrid(object):
def __init__(self, func, axes):
self.func = func
self.axes = axes
def __iter__(self):
axes = self.axes
func = self.func
name_format = "_".join(("test", self.func.__name__) +
("%s",)*len(axes))
for values in combine(axes):
name = name_format % values
yield name, make_method(func, name, values)

class genTests:
def __init__(self, *axes):
self.axes = axes
def __call__(self, func):
return TestGrid(func, self.axes)

class GridTestCase(unittest.TestCase):
class __metaclass__(type):
def __new__(mcl, clsname, bases, dict):
for name, value in dict.items():
if isinstance(value, TestGrid):
for method_name, method in value:
if method_name in dict:
raise Exception("name clash")
dict[method_name] = method
return type.__new__(mcl, clsname, bases, dict)


class MyTestCase(GridTestCase):
@genTests(["Buy", "Sell"], [1,2,3], [True, False])
def something(self, side, price, someFlag):
pass #print "test(side=%r, price=%r, someFlag=%r)" % (side, price,
someFlag)

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

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,768
Messages
2,569,575
Members
45,053
Latest member
billing-software

Latest Threads

Top