Is it possible to make a unittest decorator to rename a method from "x" to "testx?"
Peter Otten
__peter__ at web.de
Thu Aug 8 04:32:57 EDT 2013
adam.preble at gmail.com wrote:
> 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
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
More information about the Python-list
mailing list