yellowbowlmigration
yellowbowlmigration

Reputation: 143

unit test for decorator in a python package by patching flask request

I have a python module security.py which defines a decorator authorized().

I want to test the decorator. The decorator will receive a flask request header. The decorator is sth like this:

def authorized():
    def _authorized(wrapped_func):
        def _wrap(*args, **kwargs):
            if 'token' not in request.headers:
                LOG.warning("warning")
                abort(401)
                return None
            return wrapped_func(*args, **kwargs)
        return _wrap
    return _authorized

I want to mock the flask request header using a @patch decorator.The test I wrote is sth like this:

@patch('security.request.headers', Mock(side_effect=lambda *args, **kwargs: MockHeaders({})))
def test_no_authorization_token_in_header(self):
    @security.authorized()
    def decorated_func(token='abc'):
        return access_token

    result = decorated_func()
    self.assertEqual(result, None)

class MockHeaders(object):
    def __init__(self, json_data):
        self.json_data=json_data

but I always get the following error:

name = 'request'

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
       raise RuntimeError(_request_ctx_err_msg)

       RuntimeError: Working outside of request context.

       This typically means that you attempted to use functionality that needed
       an active HTTP request.  Consult the documentation on testing for
       information about how to avoid this problem.

How should I do it right?

Upvotes: 4

Views: 2400

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121266

Mock the whole request object to avoid triggering the context lookup:

@patch('security.request')

and build up the mock from there:

@patch('security.request')
def test_no_authorization_token_in_header(self, mock_request):
    mock_request.headers= {}

    @security.authorized()
    def decorated_func(token='abc'):
        return token

    self.assertRaises(Abort):
        result = decorated_func()

Since a missing token results in an Abort exception being raised, you should explicitly test for that. Note that the request.headers attribute is not called anywhere, so side_effect or return_value attributes don't apply here.

I ignored MockHeaders altogether; your decorator is not using json_data and your implementation is lacking a __contains__ method, so in tests wouldn't work on that. A plain dictionary suffices for the current code-under-test.

Side note: authorized is a decorator factory, but it doesn't take any parameters. It'd be clearer if you didn't use a factory there at all. You should also use functools.wraps() to ensure that any metadata other decorators add are properly propagated:

from functools import wraps

def authorized(wrapped_func):
    @wraps(wrapped_func)
    def _wrap(*args, **kwargs):
        if 'token' not in request.headers:
            LOG.warning("warning")
            abort(401)
            return None
        return wrapped_func(*args, **kwargs)
    return _wrap

then use the decorator directly (so no call):

@security.authorized
def decorated_func(token='abc'):
    return access_token

Upvotes: 6

Related Questions