Is it possible to make a unittest decorator to rename a method from"x" to "testx?"

A

adam.preble

We were coming into Python's unittest module from backgrounds in nunit, where they use a decorate to identify tests. So I was hoping to avoid the convention of prepending "test" to the TestClass methods that are to be actually run. I'm sure this comes up all the time, but I mean not to have to do:

class Test(unittest.TestCase):
def testBlablabla(self):
self.assertEqual(True, True)

But instead:
class Test(unittest.TestCase):
@test
def Blablabla(self):
self.assertEqual(True, True)

This is admittedly a petty thing. I have just about given up trying to actually deploy a decorator, but I haven't necessarily given up on trying to do it for the sake of knowing if it's possible.

Superficially, you'd think changing a function's __name__ should do the trick, but it looks like test discovery happens without looking at the transformed function. I tried a decorator like this:

def prepend_test(func):
print "running prepend_test"
func.__name__ = "test" + func.__name__

def decorator(*args, **kwargs):
return func(args, kwargs)

return decorator

When running unit tests, I'll see "running prepend_test" show up, but a diron the class being tested doesn't show a renamed function. I assume it only works with instances. Are there any other tricks I could consider?
 
T

Terry Reedy

We were coming into Python's unittest module from backgrounds in nunit, where they use a decorate to identify tests. So I was hoping to avoid the convention of prepending "test" to the TestClass methods that are to be actually run. I'm sure this comes up all the time, but I mean not to have to do:

class Test(unittest.TestCase):
def testBlablabla(self):
self.assertEqual(True, True)

But instead:
class Test(unittest.TestCase):
@test
def Blablabla(self):
self.assertEqual(True, True)

I cannot help but note that this is *more* typing. But anyhow, something
like this might work.

def test(f):
f.__class__.__dict__['test_'+f.__name__]

might work. Or maybe for the body just
setattr(f.__class__, 'test_'+f.__name__)

Superficially, you'd think changing a function's __name__ should do the trick, but it looks like test discovery happens without looking at the transformed function.

I am guessing that unittest discovery for each class is something like

if isinstance (cls, unittest.TestCase):
for name, f in cls.__dict__.items():
if name.startswith('test'):
yield f

You were thinking it would be
....
for f in cls.__dict__.values():
if f.__name__.startwith('test'):
yield f

Not ridiculous, but you seem to have disproven it. I believe you can
take 'name' in the docs to be bound or namespace name rather than
definition or attribute name.
 
P

Peter Otten

We were coming into Python's unittest module from backgrounds in nunit,
where they use a decorate to identify tests. So I was hoping to avoid the
convention of prepending "test" to the TestClass methods that are to be
actually run. I'm sure this comes up all the time, but I mean not to have
to do:

class Test(unittest.TestCase):
def testBlablabla(self):
self.assertEqual(True, True)

But instead:
class Test(unittest.TestCase):
@test
def Blablabla(self):
self.assertEqual(True, True)

This is admittedly a petty thing. I have just about given up trying to
actually deploy a decorator, but I haven't necessarily given up on trying
to do it for the sake of knowing if it's possible.

Superficially, you'd think changing a function's __name__ should do the
trick, but it looks like test discovery happens without looking at the
transformed function. I tried a decorator like this:

def prepend_test(func):
print "running prepend_test"
func.__name__ = "test" + func.__name__

def decorator(*args, **kwargs):
return func(args, kwargs)

return decorator

When running unit tests, I'll see "running prepend_test" show up, but a
dir on the class being tested doesn't show a renamed function. I assume
it only works with instances. Are there any other tricks I could
consider?

I think you are misunderstanding what a decorator does. You can think of

def f(...): ...

as syntactic sugar for an assignment

f = make_function(...)

A decorator intercepts that

f = decorator(make_function(...))

and therefore can modify or replace the function object, but has no
influence on the name binding.

For unittest to allow methods bound to a name not starting with "test" you
have to write a custom test loader.

import functools
import unittest.loader
import unittest

def test(method):
method.unittest_method = True
return method

class MyLoader(unittest.TestLoader):

def getTestCaseNames(self, testCaseClass):
def isTestMethod(attrname, testCaseClass=testCaseClass,
prefix=self.testMethodPrefix):
attr = getattr(testCaseClass, attrname)
if getattr(attr, "unittest_method", False):
return True
return attrname.startswith(prefix) and callable(attr)

testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
if self.sortTestMethodsUsing:
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
return testFnNames

class A(unittest.TestCase):
def test_one(self):
pass
@test
def two(self):
pass

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

Alternatively you can write a metaclass that *can* intercept the name
binding process:

$ cat mytestcase.py
import unittest

__UNITTEST = True

PREFIX = "test_"

class Type(type):
def __new__(class_, name, bases, classdict):
newclassdict = {}
for name, attr in classdict.items():
if getattr(attr, "test", False):
assert not name.startswith(PREFIX)
name = PREFIX + name
assert name not in newclassdict
newclassdict[name] = attr
return type.__new__(class_, name, bases, newclassdict)

class MyTestCase(unittest.TestCase, metaclass=Type):
pass

def test(method):
method.test = True
return method
$ cat mytestcase_demo.py
import unittest
from mytestcase import MyTestCase, test

class T(MyTestCase):
def test_one(self):
pass
@test
def two(self):
pass

if __name__ == "__main__":
unittest.main()
$ python3 mytestcase_demo.py -v
test_one (__main__.test_two) ... ok
test_two (__main__.test_two) ... ok
 
P

Peter Otten

Peter said:
$ python3 mytestcase_demo.py -v
test_one (__main__.test_two) ... ok
test_two (__main__.test_two) ... ok

Oops, that's an odd class name. Fixing the name clash in Types.__new__() is
left as an exercise...
 
A

adam.preble

Peter Otten wrote:
Oops, that's an odd class name. Fixing the name clash in Types.__new__() is

left as an exercise...

I will do some experiments with a custom test loader since I wasn't aware of that as a viable alternative. I am grateful for the responses.
 
A

adam.preble

I cannot help but note that this is *more* typing. But anyhow, something

It wasn't so much about the typing so much as having "test" in front of everything. It's a problem particular to me since I'm writing code that, well, runs experiments. So the word "test" is already all over the place. I would even prefer if I could do away with assuming everything starting with "test" is a unittest, but I didn't think I could; it looks like Peter Ottengot me in the right direction.
like this might work.
def test(f):

f.__class__.__dict__['test_'+f.__name__]



might work. Or maybe for the body just

setattr(f.__class__, 'test_'+f.__name__)

Just for giggles I can mess around with those exact lines, but I did get spanked trying to do something similar. I couldn't reference __class__ for some reason (Python 2.7 problem?).
 
N

Ned Batchelder

I will do some experiments with a custom test loader since I wasn't aware of that as a viable alternative. I am grateful for the responses.
If you can use another test runner, they often have more flexible and
powerful ways to do everything. nosetests will let you use a __test__
attribute, for example, to mark tests. Your decorator could simply
assign that attribute on the test methods.

You'd still write your tests using the unittest base classes, but run
them with nose.

--Ned.
 
T

Terry Reedy

def test(f):

f.__class__.__dict__['test_'+f.__name__]

Sorry, f.__class__ is 'function', not the enclosing class. A decorator
for a method could not get the enclosing class name until 3.3, when it
would be part of f.__qualname__.

Use one of the other suggestions.
Just for giggles I can mess around with those exact lines, but I did get spanked trying to do something similar. I couldn't reference __class__ for some reason (Python 2.7 problem?).

In 2.x, old-style classes and instances thereof do not have .__class__.
All other objects do, as far as I know.
 
A

adam.preble

Peter Otten wrote:
Oops, that's an odd class name. Fixing the name clash in Types.__new__() is

left as an exercise...

Interesting, I got __main__.T, even though I pretty much just tried your code wholesale. For what it's worth, I'm using Python 2.7. I'm glad to see that code since I learned a lot of tricks from it.
 
P

Peter Otten

Interesting, I got __main__.T, even though I pretty much just tried your
code wholesale.

I see I have to fix it myself then...
For what it's worth, I'm using Python 2.7. I'm glad to
see that code since I learned a lot of tricks from it.

[My buggy code]
class Type(type):
def __new__(class_, name, bases, classdict):

Here 'name' is the class name
newclassdict = {}
for name, attr in classdict.items():
if getattr(attr, "test", False):
assert not name.startswith(PREFIX)
name = PREFIX + name
assert name not in newclassdict
newclassdict[name] = attr

Here 'name' is the the last key of classdict which is passed to type.__new__
instead of the actual class name.
return type.__new__(class_, name, bases, newclassdict)

[Fixed version]

class Type(type):
def __new__(class_, classname, bases, classdict):
newclassdict = {}
for name, attr in classdict.items():
if getattr(attr, "test", False):
assert not name.startswith(PREFIX)
name = PREFIX + name
assert name not in newclassdict
newclassdict[name] = attr
return type.__new__(class_, classname, bases, newclassdict)
 
A

adam.preble

I see I have to fix it myself then...

Sorry man, I think in my excitement of seeing the first of your examples to work, that I missed the second example, only seeing your comments about it at the end of the post. I didn't expect such a good response.
 

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,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top