[TIP] Problem mocking a decorator

Michael Foord fuzzyman at voidspace.org.uk
Tue Dec 28 07:49:16 PST 2010


On 28/12/2010 15:32, Yoni Tsafir wrote:
> Hi guys,
> I'm pretty new to the world of testing in python, and there's a 
> problem I ran into while trying to mock a decorator in the code I'm 
> testing.
> Tell me what am I doing wrong or if this is a missing feature in the 
> mock library.
>

The fundamental problem is that decorators are applied when the module 
is imported (when the function is *created*). Attempting to mock out the 
decorator later has no effect because the decorator has already been used.

You could either reorganise your code to have catch_bad_args delegate to 
another function that you can mock or patch both sys.stderr and sys.exit 
and test the code by its effects.

I would be tempted to have catch_bad_args call a _report_bad_args 
function with the exception instance. You could then just patch this 
function to test the behaviour.

Something like:


def catch_bad_args(func):
     def call(*args, **kwargs):
         try:
             return func(*args, **kwargs)
         except BadArgsError, e:
             _report_bad_args(e)

def _report_bad_args(e):
     print >> sys.stderr, e.args[0]
     print
     sys.exit(1)

In general having code call sys.exit directly makes it harder to test. 
If possible you should organise your application to have a single code 
path for exiting, with a single point that needs mocking / patching to 
override this behaviour.

If your application entry point looks (something) like this:

def main():
     try:
         args = handle(args)
     except BadArgsError as e:
         _handle_bad_args(e)
         return 1
     else:
         do_other_stuff()

if __name__ == '__main__':
     result = main()
     sys.exit(result)

Then you can test your main function handles bad arguments without 
worrying about it calling sys.exit().

To test the return code with bad arguments you can use subprocess or 
similar as a complete "functional test" that executes the application in 
the same way as the user would.

All the best,

Michael
> I have the following code:
>
> a.py:
> -------
> import sys
>
> class BadArgsError(Exception):
>     pass
>
>
> def catch_bad_args(func):
>     def call(*args, **kwargs):
>         try:
>             return func(*args, **kwargs)
>         except BadArgsError, e:
>             print >> sys.stderr, e.args[0]
>             print
>             sys.exit(1)
>
>     call.__name__ = func.__name__
>     call.__dict__ = func.__dict__
>     call.__doc__  = func.__doc__
>     return call
>
> b.py
> ------
> from a import BadArgsError, catch_bad_args
>
> @catch_bad_args
> def foo(arg):
>     if arg > 2:
>         raise BadArgsError("arg is bigger than 2")
>
>     print "arg is ok"
>
> test_b.py
> -------------
> import unittest
> from b import foo
> from a import BadArgsError
> from mock import patch
> def do_nothing_decorator(func):
>     return func
> class BTest(unittest.TestCase):
>         @patch("a.catch_bad_args", do_nothing_decorator)
>         def test_three_raises(self):
>                 self.assertRaises(BadArgsError, foo, 3)
>
>
> Now running the tests:
> -------------------------------
> # python test_b.py
> arg is bigger than 2
>
> E
> ======================================================================
> ERROR: test_three_raises (__main__.BTest)
> ----------------------------------------------------------------------
> Traceback (most recent call last):
>   File "test_b.py", line 13, in test_three_raises
>     self.assertRaises(BadArgsError, foo, 3)
>   File "/usr/lib/python2.6/unittest.py", line 336, in failUnlessRaises
>     callableObj(*args, **kwargs)
>   File "/tmp/a.py", line 14, in call
>     sys.exit(1)
> SystemExit: 1
>
> ----------------------------------------------------------------------
> Ran 1 test in 0.014s
>
> FAILED (errors=1)
>
>
> What am I doing wrong? I tried to change the patch to 
> "b.catch_bad_args" and I get the same results, I also tried 
> @patch.object and other combinations... No help
>
>
> Thanks a lot!
>
>
> _______________________________________________
> testing-in-python mailing list
> testing-in-python at lists.idyll.org
> http://lists.idyll.org/listinfo/testing-in-python


-- 
http://www.voidspace.org.uk/

May you do good and not evil
May you find forgiveness for yourself and forgive others
May you share freely, never taking more than you give.
-- the sqlite blessing http://www.sqlite.org/different.html

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.idyll.org/pipermail/testing-in-python/attachments/20101228/2224dba5/attachment-0001.htm>


More information about the testing-in-python mailing list