Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I patch a Python decorator before it wraps a function?

I have a function with a decorator that I'm trying test with the help of the Python Mock library. I'd like to use mock.patch to replace the real decorator with a mock 'bypass' decorator which just calls the function.

What I can't figure out is how to apply the patch before the real decorator wraps the function. I've tried a few different variations on the patch target and reordering the patch and import statements, but without success. Any ideas?

like image 795
Chris Sears Avatar asked Oct 05 '11 20:10

Chris Sears


People also ask

How does the patch decorator work Python?

It works by decorating each test method in the class. This reduces the boilerplate code when your test methods share a common patchings set. patch finds tests by looking for method names that start with patch.

Why you should wrap decorators in Python?

Python decorators are a powerful concept that allow you to "wrap" a function with another function. The idea of a decorator is to abstract away something that you want a function or class to do, besides its normal responsibility. This can be helpful for many reasons such as code reuse, and sticking to curlys law.

How do I unit test a Python decorator?

Python decorators can be easily tested by integration tests, but it is good practice to create some unit tests for them as well. You can easily unit test a decorator by applying it to a dummy function or mock.

Are decorators Pythonic?

Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of a function or class. Decorators allow us to wrap another function in order to extend the behaviour of the wrapped function, without permanently modifying it.


1 Answers

It should be noted that several of the answers here will patch the decorator for the entire test session rather than a single test instance; which may be undesirable. Here's how to patch a decorator that only persists through a single test.

Our unit to be tested with the undesired decorator:

# app/uut.py  from app.decorators import func_decor  @func_decor def unit_to_be_tested():     # Do stuff     pass 

From decorators module:

# app/decorators.py  def func_decor(func):     def inner(*args, **kwargs):         print "Do stuff we don't want in our test"         return func(*args, **kwargs)     return inner 

By the time our test gets collected during a test run, the undesired decorator has already been applied to our unit under test (because that happens at import time). In order to get rid of that, we'll need to manually replace the decorator in the decorator's module and then re-import the module containing our UUT.

Our test module:

#  test_uut.py  from unittest import TestCase from app import uut  # Module with our thing to test from app import decorators  # Module with the decorator we need to replace import imp  # Library to help us reload our UUT module from mock import patch   class TestUUT(TestCase):     def setUp(self):         # Do cleanup first so it is ready if an exception is raised         def kill_patches():  # Create a cleanup callback that undoes our patches             patch.stopall()  # Stops all patches started with start()             imp.reload(uut)  # Reload our UUT module which restores the original decorator         self.addCleanup(kill_patches)  # We want to make sure this is run so we do this in addCleanup instead of tearDown          # Now patch the decorator where the decorator is being imported from         patch('app.decorators.func_decor', lambda x: x).start()  # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()                   # HINT: if you're patching a decor with params use something like:         # lambda *x, **y: lambda f: f         imp.reload(uut)  # Reloads the uut.py module which applies our patched decorator 

The cleanup callback, kill_patches, restores the original decorator and re-applies it to the unit we were testing. This way, our patch only persists through a single test rather than the entire session -- which is exactly how any other patch should behave. Also, since the clean up calls patch.stopall(), we can start any other patches in the setUp() we need and they will get cleaned up all in one place.

The important thing to understand about this method is how the reloading will affect things. If a module takes too long or has logic that runs on import, you may just need to shrug and test the decorator as part of the unit. :( Hopefully your code is better written than that. Right?

If one doesn't care if the patch is applied to the whole test session, the easiest way to do that is right at the top of the test file:

# test_uut.py  from mock import patch patch('app.decorators.func_decor', lambda x: x).start()  # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!  from app import uut 

Make sure to patch the file with the decorator rather than the local scope of the UUT and to start the patch before importing the unit with the decorator.

Interestingly, even if the patch is stopped, all the files that already imported will still have the patch applied to the decorator, which is the reverse of the situation we started with. Be aware that this method will patch any other files in the test run that are imported afterwards -- even if they don't declare a patch themselves.

like image 164
user2859458 Avatar answered Oct 29 '22 07:10

user2859458