Archive for October, 2010

Unit Testing Complex Views in Django with RequestFactory and Mocker

Django’s out of the box test client works great for testing simple views.  You can pass it a url, execute a view, and check the returned response for what you expected.
However, sometimes you need to test multiple views that depend on something changing in the request, or even spoofing/mocking an external call.  I’ll show you how to do both here.
We use this successfully at Movity to test multi-view OAuth scenarios that depend on funky handshakes to work properly.

Let’s take a simple test case:

from django.test import TestCase

class YourFancyTest(TestCase):

    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_your_tricky_view(self):
        response = self.client.get('/something/')
        self.assertEqual(response.status_code, 200)

So far so good.  But what if you want to change something about the request, e.g. passing in a different user?  To solve this we’ll use this RequestFactory snippet.

RequestFactory (adapted from Simon Willison’s snippet) lets you simulate a request that can be passed into a raw view function.  This won’t test your urlconf, though.  Let’s see what it looks like.

from django.test import TestCase

from yourproject.yourutils import RequestFactory  # I assume you put RequestFactory in this module
from yourproject.yourapp.views import something_view

class YourFancyTest(TestCase):

    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_your_tricky_view(self):
        # Create the new request
        rf = RequestFactory()
        request = rf.get('/something/')

        # Get a specific user and log that user in (or change the user's name, or whatever)
        user = User.objects.get(id=1)
        login(request, user)

        # Call the view directly with the new request
        response = something_view(request)
        self.assertEqual(response.status_code, 200)

Ok, so we can modify the request object.  What if you have a view that makes an external call (e.g. to an API) and the view depends on a successful response?

Ideally, your tests are self-contained and aren’t calling external services.  The problem is, you don’t want to change the view itself to do something different for a test case.
So we have to find a way to inject a response at the right time to the API call and have the view think that it came from the service.

Fortunately, you use the excellent Mocker tool to help you with this.  The way it works is that you create a mock request and pass that into the view you’re testing.  You can instruct the mock to return the right answer at the right time.

Mocker uses a record-replay model.  You specify actions for an object, then supply the fake data you want to inject.  When you replay, calling the same action returns the fake data.
To change the request object, we’ll use the .proxy() functionality, which means we’ll mask an object and only replace a few key function calls.
You can also replace a whole function call from any module, which we’ll do using .replace().

from mocker import Mocker

from django.test import TestCase

from yourproject.yourutils import RequestFactory  # I assume you put RequestFactory in this module
from yourproject.yourapp.views import something_view

class YourFancyTest(TestCase):

    def setUp(self):
        self.mocker = Mocker()

    def tearDown(self):
        pass

    def test_your_tricky_view(self):
        # Create the new (real) request
        rf = RequestFactory()
        request = rf.get('/something/')

        # Get a specific user and log that user in (or change the user's name, or whatever)
        user = User.objects.get(id=1)
        login(request, user)

        # Create the mock object. Proxy means we'll use the original object but we'll replace a few functions on it.
        # mock_request will look like a Request object for most purposes.
        mock_request = self.mocker.proxy(request)

        # Record: Let's replace a session variable.
        mock_request.session['your_session_variable']
        # This will 'inject' the data 'Mocked!' for when the view calls request.session['your_session_variable']
        self.mocker.result('Mocked!')

        # Record: Let's replace a function call.  Assume it has 3 parameters.
        mock_fn1 = self.mocker.replace('yourproject.yourapp.utils.get_status_count')
        mock_fn1(ANY, ANY, ANY)  # Tell the mock that get_status_count takes 3 params, and to match any call to it.
        # This will inject 10 as the status call result, regardless of what the function would have returned.
        self.mocker.result(10)

        # Important: this internally calls self.mocker.replay(), which enables the mock objects to return data.
        # It also cleans up a few things after the with exits.  You'll need Python 2.6+ or have to future import 'with'.
        with self.mocker:
            # Call the view directly with the mock request.
            response = something_view(mock_request)
            self.assertEqual(response.status_code, 200)

        # Clear any existing mocks, e.g. the above proxy/replace. This is important if you're calling multiple views in the same test.
        self.mocker.reset()

Now, when the view runs and tries to get the session variable and the function call, it will use the mocked values instead of calling the function.
It will only do this once for each though – if you have multiple calls you’ll have to change the count within the mocker.

Note that if you only need to override a function call, you don’t need to create a mock request object and pass it into the view.
You should be able to use .replace() to get at the function that you need.  You still have to use with self.mocker though.

To mock an external api call, just replace e.g. httplib2.Http with your mocked function and return whatever you want in the mock.

Let me know if you find this post helpful. 🙂

, , ,

1 Comment