Overriding decorator during unit test in python
Asked Answered
J

1

10

I have a django class based view that I'm decorating. Unfortunately that decorator makes outside calls to do status checks which is outside the scope of what the unit test should do so I want to override the decorator to do nothing during my unit tests. Here is my decorator:

decorators.py

def status_check(func):
    @wraps(func)
    def wrapped(request, *args, **kwargs):

        uri = settings.SERVER_URI
        status_code = None
        bad_status = [404, 500]

        try:
            response = requests.head(uri)
        except requests.ConnectionError as err:
            LOGGER.error('Server is hosed! Oh Noes! Error: %s ' % (err))
            raise Http404
        except Exception as err:
            LOGGER.error('Some crazy stuff is happening. Its Bad. '
                         'Error: %s' % (err))
            raise Http404

        status_code = response.status_code
        if not status_code or status_code in bad_status:
            LOGGER.error('Awww Snap! Server is not happy: %s' % (status_code))
            raise Http404
        return func(request, *args, **kwargs)
    return wrapped

views.py

class HandleFoo(DetailView):
    @method_decorator(status_check)
    def post(self, request):
        # do stuff here

tests.py

class RunTest(TestCase):
    def test_post(self):
        post_data = json.dumps({'stuff': 'vodka', 'things': 'tonic'})
        resp = self.client.post(self.foo_uri,
                                post_data,
                                content_type='application/json',
                               )
        self.assertEqual(resp.status_code, 200)

So is there a way for me to either override the decorator or can I bypass it altogether? I'm rather stumped on this.

EDIT Tried mocking out the request using the following from krak3n:

@patch('app.views.method_decorator.status_check', lambda func: func)
@patch('app.views.status_check', lambda func: func)
@patch('app.decorators.status_check', lambda func: func)
@patch('app.views.HandleFoo.post', lambda func: func)

The last method gets me the closest thus far, but it ends up throwing a stacktrace:

Traceback (most recent call last):
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "/Users/squiddly/projects/tests/app/tests.py", line 165, in test_post
    content_type='application/json',
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/client.py", line 463, in post
    response = super(Client, self).post(path, data=data, content_type=content_type, **extra)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/client.py", line 297, in post
    return self.request(**r)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/client.py", line 406, in request
    response = self.handler(environ)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/client.py", line 111, in __call__
    response = self.get_response(request)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/core/handlers/base.py", line 178, in get_response
    response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/core/handlers/base.py", line 224, in handle_uncaught_exception
    return callback(request, **param_dict)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/utils/decorators.py", line 91, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/views/defaults.py", line 41, in server_error
    return http.HttpResponseServerError(template.render(Context({})))
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 140, in render
    return self._render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/utils.py", line 65, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 830, in render
    bit = self.render_node(node, context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/debug.py", line 74, in render_node
    return node.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/loader_tags.py", line 124, in render
    return compiled_parent._render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/utils.py", line 65, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 830, in render
    bit = self.render_node(node, context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/debug.py", line 74, in render_node
    return node.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/loader_tags.py", line 156, in render
    return self.render_template(self.template, context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/loader_tags.py", line 138, in render_template
    output = template.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 140, in render
    return self._render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/test/utils.py", line 65, in instrumented_test_render
    return self.nodelist.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 830, in render
    bit = self.render_node(node, context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/debug.py", line 74, in render_node
    return node.render(context)
  File "/Users/squiddly/envs/testenv/lib/python2.7/site-packages/django/template/base.py", line 1185, in render
    _dict = func(*resolved_args, **resolved_kwargs)
  File "/Users/squiddly/projects/tests/app/templatetags/app_extras.py", line 40, in get_data
    if request.session.has_key('start_time'):
AttributeError: 'str' object has no attribute 'session'
Jens answered 29/3, 2013 at 6:25 Comment(0)
P
7

I think your gonna have to get into the dark underworld of Mock, but once you get your head around it (if you haven't already) the dark underworld turns into a bright blue heavenly sky of mockiness.

You could use the patch module of Mock to to patch this decorator so your views using it can become more testable: http://www.voidspace.org.uk/python/mock/patch.html. Personally I have not tried mocking a decorator before but it should work...

@patch('python.path.to.decorator', new_callable=PropertyMock)
def my_test(self, decorator_mock):
    # your test code

Give that a whirl.

You can read about the patch module in Mock here: http://www.voidspace.org.uk/python/mock/patch.html

edit

new_callable=PropertyMock is probably not the right thing to do for patching a decorator.

Perhaps try:

@patch('python.path.to.decorator', lambda: func: func)
def my_test(self):
    # your test code

This should in theory patch the decorator so it just returns the function back rather than does all the stuff you have in wrapped.

Photoconduction answered 29/3, 2013 at 10:13 Comment(4)
Thanks for the help. Tried both methods, but they're still calling the decorator: @patch('app.decorators.status_check', lambda func: func) and @patch('app.decorators.status_check', new_callable=PropertyMock)Jens
It's ok, i'm sure there would be a way to do this. I've probably not given the correct code :( This answer is probably more helpful: #7668067Photoconduction
I tried that solution before I posted, but it didn't work. :/ I think it's going to have to be a mix of the two, but how to achieve that, I'm unsure. I'll give it another go today. Thanks for the help. Much appreciated.Jens
After trying to make this work for a few days I finally figured out the "why" of it not working. When you use from django.core.urlresolvers import reverse and then use reverse('anything that points to the views') it loads those views up before the decorator can be patched. So if I hard code foo_uri instead of using reverse, I can use @patch('app.views.status_check', lambda func: func) and it works.Jens

© 2022 - 2024 — McMap. All rights reserved.