Django Unit Testing with Mocks

Here I’m going to talk about how to use mocks when writing unit tests for Django. Using Django test mocks has really opened my eyes on how much better unit tests can be written.

Integration Vs Unit Tests

At work, our Django web app is using an internal API and it’s using the cache framework. At first when writing unit tests for the code I would run either the internal API server or run my fake Python API server which serves up default HTTP responses. For caching I would just switch to the DummyCacheBackend that comes with Django in my settings file.

However I realized these would be integration tests; tests that have multiple moving parts and cannot test the code in isolation. They weren’t unit tests at all.

Using Mocks

That’s when I started adding mocks and patching functions and classes to return the right values for testing. For Python and Django I’m using the mock library.

Testing Without Mocks

So a typical test would look something like this without Django mocks and hitting the real API:

from django.test import TestCase
from myapp.models import MyModel

class MyModelTestCase(TestCase):
    def test_api_call(self):
        MyModel.do_api_call()
        self.assertEqual(MyModel.api_value, 1)

Adding Mocks To Unit Tests

Then I would add the mocks:

from django.test import TestCase
from mock import patch
from myapp.models import MyModel

class MyModelTestCase(TestCase):
    @patch('internal_api.api_call', return_value=1)
    def test_api_call(self, mock_api_call):
        MyModel.do_api_call()
        self.assertEqual(MyModel.api_value, 1)
        self.assertEqual(mock_api_call.call_count, 1)

Cleaner Re-Usable Mocks

Finally when we have multiple tests that uses the same mocks over and over again, we have to refactor that into the setUp method:

from django.test import TestCase
from mock import patch
from myapp.models import MyModel

class MyModelTestCase(TestCase):
    def setUp(self):
        super(MyModelTestCase, self).setUp()
        patcher_api_call = patch('internal_api.api_call')
        self.mock_api_call = patcher_api_call.start()
        self.addCleanup(patcher_api_call.stop)

    def test_api_call(self):
        self.mock_api_call.return_value = 1
        MyModel.do_api_call()
        self.assertEqual(MyModel.api_value, 1)

Unit tests shouldn’t be treated like lesser code. It should be as clean as possible and don’t ever hesitate when it comes to refactoring the unit test code. It can save you a lot of trouble in the long run.

Mocking The Cache

One important note about mocking the Django cache. When you import the cache from django.core.cache, you’re instantiating the cache object at the place it’s being imported.

Let’s say your code looks like this:

# models.py
from django.db import models
from django.core.cache import cache

class MyModel(models.Model):
    field = models.IntegerField()
    def do_something(self):
        cache.set('hello-world', self.field)

That means when you’re patching it for a unit test you’re going to want to do this:

# tests.py
from django.test import TestCase
from mock import patch
from myapp.models import MyModel

class MyModelTestCase(TestCase):
    def setUp(self):
        patcher_cache = patch('myapp.models.cache')
        self.mock_cache = patcher_cache.start()
        self.addCleanup(patcher_cache.stop)

    def test_do_something(self):
        """
        When do_something is called, cache.set is called exactly one
        """
        mymodel = MyModel()
        mymodel.do_something()
        self.assertEqual(self.mock_cache.set.call_count, 1)

I hope this brief article helps someone else out there improve their code base and make it more testable.

References

  1. mock library for Python
  2. patch methods: start and stop
  3. where to patch
  4. TestCase.addCleanup

More On Django! (January 2017 update)

For more on Django, check out the book Two Scoops of Django: Best Practices. Bought a copy and it’s wonderful, it helped our team figure out the right way to structure settings.py for production, staging and development environments.

The 2nd edition of Test-Driven Development with Python is going to be released at the end of June 2017. It features a section on testing with Django and Selenium.

Comments are Closed