Testing Python Decorators?

Question:

I’m writing some unit tests for a Django project, and I was wondering if its possible (or necessary?) to test some of the decorators that I wrote for it.

Here is an example of a decorator that I wrote:

class login_required(object):

    def __init__(self, f):
        self.f = f

    def __call__(self, *args):
        request = args[0]
        if request.user and request.user.is_authenticated():
            return self.f(*args)
        return redirect('/login')
Asked By: Jama22

||

Answers:

A decorator like this might be tested simply thanks to duck-typing. Just supply a mock object to the call function, that seems to hold and act as a request, and see if you get the expected behaviour.

When it is necessary to use unit tests is quite individual i’d say. The example you give contain such basic code that one might say that it isn’t necessary. But then again, the cost of testing a class like this is equally low.

Answered By: daramarak

Simply:

from nose.tools import assert_equal
from mock import Mock

class TestLoginRequired(object):
    def test_no_user(self):
        func = Mock()
        decorated_func = login_required(func)
        request = prepare_request_without_user()
        response = decorated_func(request)
        assert not func.called
        # assert response is redirect

    def test_bad_user(self):
        func = Mock()
        decorated_func = login_required(func)
        request = prepare_request_with_non_authenticated_user()
        response = decorated_func(request)
        assert not func.called
        # assert response is redirect

    def test_ok(self):
        func = Mock(return_value='my response')
        decorated_func = login_required(func)
        request = prepare_request_with_ok_user()
        response = decorated_func(request)
        func.assert_called_with(request)
        assert_equal(response, 'my response')

The mock library helps here.

Answered By: nkrkv

Example for Django’s UnitTest

class TestCaseExample(TestCase):
    def test_decorator(self):
        request = HttpRequest()
        # Set the required properties of your request
        function = lambda x: x
        decorator = login_required(function)
        response = decorator(request)
        self.assertRedirects(response)

In general, the approach I’ve utilized is the following:

  1. Set up your request.
  2. Create a dummy function to allow the decorator magic to happen (lambda). This is where you can control the number of arguments that will eventually be passed into the decorator.
  3. Conduct an assertion based on your decorator’s response.
Answered By: Hassan

For those looking for a django type decorator test, this is how I ran tests on my custom django decorator:

common/decorators.py

from functools import wraps
from django.http import Http404


def condition_passes_test(test_func, fail_msg=''):
    """
    Decorator for views that checks that a condition passes the given test,
    raising a 404 if condition fails
    """

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            if test_func():
                return view_func(request, *args, **kwargs)
            else:
                raise Http404(fail_msg)
        return _wrapped_view
    return decorator

The test:

import django
from django.test import TestCase
from django.http import Http404
from django.http import HttpResponse
from django.test import RequestFactory

from common import decorators


class TestCommonDecorators(TestCase):

    def shortDescription(self):
        return None

    def test_condition_passes_test1(self):
        """
        Test case where we raise a 404 b/c test function returns False
        """
        def func():
            return False

        @decorators.condition_passes_test(func)
        def a_view(request):
            return HttpResponse('a response for request {}'.format(request))
        request = RequestFactory().get('/a/url')

        with self.assertRaises(django.http.response.Http404):
            a_view(request)

    def test_condition_passes_test2(self):
        """
        Test case where we get 200 b/c test function returns True
        """
        def func():
            return True

        @decorators.condition_passes_test(func)
        def a_view(request):
            return HttpResponse('a response for request {}'.format(request))

        request = RequestFactory().get('/app_health/z')
        request = a_view(request)
        self.assertEquals(request.status_code, 200)

NOTE: I Am using it as such on my view where my_test_function returns True or False:

@method_decorator(condition_passes_test(my_test_function, fail_msg="Container view disabled"), name='dispatch')
Answered By: radtek