Search code examples
pythonpytestmonkeypatching

pytest-monkeypatch a decorator (not using mock / patch)


I am writing some tests using pytest with the monkeypatch fixture. Following the rules I am importing the classes and methods to mock out from the module they are being used in and not from the source.

The application I am writing tests for is a Google App Engine application which uses the Standard environment. As such I have to use python 2.7, the actual version I am using is 2.7.15 - pytest version is 3.5.0

Everything has been working well so far but I have hit a problem when trying to mock out a decorator function.

Starting from the top. In a py file called decorators.py contains all the auth decorators including the decorator I want to mock out. The decorator in question is a module function, not part of a class.

def user_login_required(handler):
    def is_authenticated(self, *args, **kwargs):
        u = self.auth.get_user_by_session()
        if u.access == '' or u.access is None:
            # return the response
            self.redirect('/admin', permanent=True)
        else:
            return handler(self, *args, **kwargs)
    return is_authenticated

The decorator is applied to the web request function. A basic example in a file called UserDetails.py in a folder called handlers (handlers.UserDetails)

from decorators import user_login_required

class UserDetailsHandler(BaseHandler):
    @user_login_required
    def get(self):
        # Do web stuff, return html, etc

In a test module I am setting up the test like this:

from handlers.UserDetails import user_login_required

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(user_login_required, mock_user_login_required_func)

The problem with this is that monkeypatch does not allow me to put a single function in as the target. It wants the target to be a Class, followed by the method name to be replaced then the mock method....

monkeypatch.setattr(WouldBeClass, "user_login_required", mock_user_login_required_func)

I have tried to adjust the code to see if I can get round it by changing how the decorator is imported and used like this:

import decorators

class UserDetailsHandler(BaseHandler):
    @decorators.user_login_required
    def get(self):
        # Do web stuff, return html, etc

Then in the test I try to patch the function name like so.....

from handlers.UserDetails import decorators

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(decorators, "user_login_required" , mock_user_login_required_func)

Although this code does not throw any errors, when I step through the test the code never enters the mock_user_login_required_func. It always goes into the live decorator.

What am I doing wrong? This this a problem with trying to monkeypatch decorators in general or can lone functions in modules not be patched?


Solution

  • It looks like the quick answer here is simply to move your Handler import so that it occurs after the patch. The decorator and the decorated functions must be in separate modules so that python doesn’t execute the decorator before you’ve patched it.

    from decorators import user_login_required
    
    @pytest.mark.parametrize('params', get_params, ids=get_ids)
    def test_post(self, params, monkeypatch):
    
        monkeypatch.setattr(decorators, "user_login_required" , mock_user_login_required_func)
        from handlers.UserDetails import UserDetailsHandler
    

    You may find this easier to accomplish using the patch function from the built in unittest.mock module.