Login | Register For Free | Help
Search for: (Advanced)

Mailing List Archive: Python: Python

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

 

 

Python python RSS feed   Index | Next | Previous | View Threaded


adam.preble at gmail

Aug 7, 2013, 11:32 PM

Post #1 of 11 (41 views)
Permalink
Is it possible to make a unittest decorator to rename a method from "x" to "testx?"

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?
--
http://mail.python.org/mailman/listinfo/python-list


tjreedy at udel

Aug 8, 2013, 1:04 AM

Post #2 of 11 (40 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On 8/8/2013 2:32 AM, adam.preble [at] gmail 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)

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.

--
Terry Jan Reedy

--
http://mail.python.org/mailman/listinfo/python-list


__peter__ at web

Aug 8, 2013, 1:32 AM

Post #3 of 11 (37 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

adam.preble [at] gmail 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


--
http://mail.python.org/mailman/listinfo/python-list


__peter__ at web

Aug 8, 2013, 1:50 AM

Post #4 of 11 (37 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

Peter Otten wrote:

> $ python3 mytestcase_demo.py -v
> test_one (__main__.test_two) ... ok
> test_two (__main__.test_two) ... ok
>
> ----------------------------------------------------------------------
> Ran 2 tests in 0.000s

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

--
http://mail.python.org/mailman/listinfo/python-list


adam.preble at gmail

Aug 8, 2013, 9:17 AM

Post #5 of 11 (33 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On Thursday, August 8, 2013 3:50:47 AM UTC-5, Peter Otten wrote:
> 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.
--
http://mail.python.org/mailman/listinfo/python-list


adam.preble at gmail

Aug 8, 2013, 9:20 AM

Post #6 of 11 (33 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On Thursday, August 8, 2013 3:04:30 AM UTC-5, Terry Reedy wrote:
> 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 Otten got 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?).
--
http://mail.python.org/mailman/listinfo/python-list


ned at nedbatchelder

Aug 8, 2013, 10:14 AM

Post #7 of 11 (31 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On 8/8/13 12:17 PM, adam.preble [at] gmail wrote:
> On Thursday, August 8, 2013 3:50:47 AM UTC-5, Peter Otten wrote:
>> 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.
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.
--
http://mail.python.org/mailman/listinfo/python-list


tjreedy at udel

Aug 8, 2013, 12:28 PM

Post #8 of 11 (30 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On 8/8/2013 12:20 PM, adam.preble [at] gmail wrote:
> On Thursday, August 8, 2013 3:04:30 AM UTC-5, Terry Reedy wrote:

>> 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.

--
Terry Jan Reedy

--
http://mail.python.org/mailman/listinfo/python-list


adam.preble at gmail

Aug 8, 2013, 10:07 PM

Post #9 of 11 (27 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On Thursday, August 8, 2013 3:50:47 AM UTC-5, Peter Otten wrote:
> 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.
--
http://mail.python.org/mailman/listinfo/python-list


__peter__ at web

Aug 8, 2013, 11:31 PM

Post #10 of 11 (26 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

adam.preble [at] gmail wrote:

> On Thursday, August 8, 2013 3:50:47 AM UTC-5, Peter Otten wrote:
>> 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.

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)


--
http://mail.python.org/mailman/listinfo/python-list


adam.preble at gmail

Aug 11, 2013, 9:25 PM

Post #11 of 11 (6 views)
Permalink
Re: Is it possible to make a unittest decorator to rename a method from "x" to "testx?" [In reply to]

On Friday, August 9, 2013 1:31:43 AM UTC-5, Peter Otten wrote:
> 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.
--
http://mail.python.org/mailman/listinfo/python-list

Python python RSS feed   Index | Next | Previous | View Threaded
 
 


Interested in having your list archived? Contact Gossamer Threads
 
  Web Applications & Managed Hosting Powered by Gossamer Threads Inc.