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