How a Django Developer Can Write Speedy Unit Tests, Pt. 2: Fake it ‘Til You Make It

This post is the second of a two-part series on writing fast and efficient unit tests in Python/Django. If you missed part one, be sure to start here.

At this point, a Django developer using Agile development methodologies will have a good handle on writing focused unit tests and your days of writing only integration tests for all of your features are long behind you. You stay aware of database transactions, and you employ agile software testing with read-only data in mind.

You’ve probably discovered this too: there are still going to be situations where it seems you’ll need to write data in unit tests. Some methods might require two related objects, the mechanics of which may not function without invoking database machinery (like a ManyToMany relationship using a through table with data populated on save). The Python testing library Mock becomes vastly useful here.

Mock is described by its development team as:

a library for testing in Python [that] allows you to replace parts of your system under test with mock objects and make assertions about how they have been used.

Okay, so that’s a little vague. Basically, at the core of the library is the Mock object. Mock is a very magical “stub” object that can be configured to behave almost any way you like. After creating a Mock, you can attach other Mock objects as attributes and set return values so they behave as callables. Or, you can set side effects to simulate the raising of exceptions. Mock objects also keep track of whether or not they have been called, how many times, and with what arguments. It can be a bit confusing to wrap your head around at first, but once you get familiar with Mocks, they’ll be your testing best friend.

Mocking an object instance

import unittest class TestCase(unittest.TestCase): def test_car_str(self): instance = Car.objects.create(make="Honda", model="Civic") self.assertEqual(instance.__str__(), "Honda Civic")

In this very basic example, the unit test is testing things that it ideally shouldn’t have to. Namely that Manager.create() creates a Car instance and uses the submitted kwargs to populate its member fields. So how can we just test the Car.__str__ method?

import mock import unittest class TestCase(unittest.TestCase): def test_car_str(self): mock_instance = mock.Mock(spec=Car) mock_instance.make = "Honda" mock_instance.model = "Civic" self.assertEqual(Car.__str__(mock_instance), "Honda Civic")

By passing the spec argument to the Mock constructor we instruct the object to respond as though it were of type Car. Then we attach arbitrary test data as attributes without the overhead of instantiating model field objects, and directly test the method we were interested in. Mock objects are useful in almost any situation, including in a place where you might normally test with a request factory object:

import mock import unittest class TestCase(unittest.TestCase): def test_my_template_tag(self): mock_request = mock.Mock() mock_request.user = mock.Mock() mock_request.user.is_superuser = mock.Mock() mock_request.user.is_superuser.return_value = True mock_request.session = {} response = my_template_tag(mock_request) # . . .

Mocks can also be configured by kwarg:

user_config = {'is_superuser.return_value': True, } mock_request.user = mock.Mock() mock_request.user.configure_mock(**user_config)

Mocking modules with the patch() decorator

So now you can create test objects limited in scope to exactly what you want to test. In the same vein, what about testing methods limited in scope to exactly what you want to test? That’s where the other significant portion of the mock library comes in: the patch decorator. The mock.patch() decorator allows to you patch modules in Python’s sys.modules and replace them with Mock objects. This is where mock gets really powerful. Take this method:

def foo(x): try: MyModel.objects.get(pk=x) except MyModel.DoesNotExist: return True return False

If we want to test foo(), we’re only interested in testing that it reacts differently based on whether or not a DoesNotExist exception is thrown–nothing else. With mock.patch, we can patch MyModel with a Mock object from within our test before foo is called:

import mock import unittest from myapp.utils import foo class TestCase(unittest.TestCase): def test_patched_foo(self): with mock.patch('myapp.utils.MyModel') as my_model_mock: from myapp.utils import foo my_model_mock.objects = mock.Mock() conf = {'get.side_effect': MyModel.DoesNotExist} my_model_mock.objects.configure_mock(**conf) self.assertFalse(foo(1)) conf = {'get.return_value': mock.Mock()} my_model_mock.objects.configure_mock(**conf) self.assertTrue(foo(1))

MyModel is no longer a Model class in the context of this test–It’s a Mock object. By patching the reference to MyModel in the namespace of myapp.utils, the foo method will actually be calling the imported mock. By manipulating the way this mock behaves (such as throwing a DoesNotExist exception when MyModel.objects.get() is called) we can test that our method reacts accordingly, without having to actually test the nuts and bolts of the Django framework.

Armed with an endless supply of mock objects and the concept of patching, you can begin testing very complex situations. Because of this we believe that Mocking should be a standard technique in unit testing Django code.

Another example, more complex this time

For this example we’re going to build out a simple authenticated view and test it. On to the code:

from django.shortcuts import render_to_response from django.contrib.auth.decorators import login_required from django.template import RequestContext @login_required def authenticated_object_view(request, arg1, arg2): tvar1, tvar2 = 0, 0 if arg1 is not None: tvar1, tvar2 = 1, 2 else: tvar1, tvar2 = 3, 4 return render_to_response('a_template.html', {'templ_var1':tvar1, 'templ_var2':tvar2}, RequestContext(request))

This is a pretty simple view, but us Django developers have more obstacles in our way this time. We are interested in verifying that the logic of the function is such that under some conditions tvar1 and tvar2 are set appropriately. What we do not want to test is that login_required works as we expect or that render_to_response works. Thus we mock both along with RequestContext to clear out obstructions to testing the precise functionality we are building out.

It will look something like:

import unittest import mock class AuthenticatedObjectViewTest(unittest.TestCase): """ This collects the tests around the functionality provided in amodule.views.authenticated_object_view """ def test_authenticated_object_view_arg1_is_not_none(self): """ Here we test the case that arg1 is not None but arg2 is none """ mock_render_to_response = mock.MagicMock() with mock.patch.multiple('amodule.views', render_to_response=mock_render_to_response, RequestContext=mock.MagicMock(), login_required=lambda x: x): from amodule.views import authenticated_object_view mock_request = mock.Mock() authenticated_object_view(mock_request, 'test1', None) _, args, _ = mock_render_to_response.mock_calls[0] self.assertEquals(args[0], 'a_template.html', 'The wrong template is in our render') self.assertEquals(args[1]['templ_var1'], 1, 'Passing the wrong template variable in') self.assertEquals(args[1]['templ_var2'], 2, 'Passing the wrong template variable in')

Let’s break it down. The mock selections are all Django framework components: login_required just becomes a null anonymous function; RequestContext is mocked (and all methods, attributes, etc. evaluate to True) though we could just as easily setup the mock_request to behave as RequestContext is expected. Notice too the mock.patch.multiple decorator is being used on the views module itself. What we are doing here is essentially doing a setattr() on the amodule.views object in sys.modules. In this way we are able to patch multiple imports simultaneously within the context of this test.

The validation comes on the assertions at the end. These three conditions are the exact permutation of outputs we expect on the provided inputs. As bugs arise they will be exposed when inputs yield unexpected outputs. Another avenue to validation, as opposed to using the assertEquals on the args passed to render_to_response is using assert_called_with on the render_to_response mock. assert_called_with will take accept a series of traditional arguments, *args, and **kwargs and raise an assertion error if the mock object was not called using those exact inputs.

If you’re new to the idea of using mock objects you might be feeling a little dizzy at this point, as it is certainly quite a bit of information to digest. And there’s plenty more where that came from, since we’ve actually still only covered a minor portion of the Mock library.

That said, we could write up examples all day long, but the best way to learn Mock after getting through the basics is to dive right in. Find an old (or current) project with a shady test suite and do away with each instance of the Django test client you encounter, and you’ll get the hang of it in no time.

A final note on test quality

We started down this path of self-improvement to ease the frustration of slow test suites for our web & mobile solutions clients described in part I, but in doing so writing fast tests evolved into a full-on testing philosophy.

By testing efficiently and with read-only data, we are testing the precise logic of the code we write without worrying about the underlying libraries or background behavior. We, through unit testing, specify the policy around these interfaces with a complete set of tests that capture all the allowed permutations of inputs and outputs, and as a powerful consequence of this we test and document simultaneously. This methodology flushes out all the different cases that designers may have overlooked and then commits to those solutions. In the future, when this code is changed to accomplish some other aim we can intelligently make choices about what functionality we are giving up in order to do so, and that’s a tremendous added benefit.

Be sure to follow up and read fully the mock documentation to find even more tools for testing efficiently.