Unit testing Django with doctest

There are two main ways to write tests in Django; doctests and unit tests. Units tests will be familiar to you if you're coming from Java. You basically write new Python code to setup and execute your tests. Doctests are a combination of documentation and unit testing. You actually write executable tests in your comments.

Why is this is a good idea? Well, for one thing, they make sure your comments are accurate. If you change how a function behaves, your doctests will let you know if you forget to update the comments. But I mainly appreciate them because of their brevity.

def _lucene_location(job):
    """Formats a Lucene query for searching by city/state

    Values must be double-quoted in case there are spaces
    >>> _lucene_location(Job(city="Fort Worth", state="TX"))
    ' +JOBCITY:"Fort Worth" +JOBSTATE:"TX"'

    Need to escape quotes in the city/state values
    >>> _lucene_location(Job(city='"Bad" City', state="MA"))
    ' +JOBCITY:"%22Bad%22 City" +JOBSTATE:"MA"'

    """
    return "".join([
        ' +%s:"%s"' % (field_solr[attr], urllib.quote(getattr(job, attr), " "))
        for attr in  ["city", "state"]
        ])

Here we have a function that takes a Job model object and formats a Lucene search string for city and state. The primary edge case, which we are testing for, is that the city name can contain a space, so it needs to be in double-quotes. But what if the name itself has a double-quote character? Then it needs to be encoded.

These conditions are covered by doctests, where ">>>" denotes a statement to be run in a Python interactive session, and where the following line is the expected result. I can't think of a way to express these tests with less code.

Doctests have some downsides. They are not as flexible as unit tests. If your tests require a complicated setup procedure, that's probably better done as a unit test. The other downside I encounted was documentation. The Django website simply tells you to place your doctests in a file called tests.py. But of course, most developers will want to execute doctests that are in-line with functions in other .py files.

The solution is easy, but was not readily apparent to me from the documentation. Simply put this in your tests.py file:

from search.models import Job
from search.placement import _lucene_location, _lucene_query, _fuzzy_titles

__test__ = {
    "_lucene_location": _lucene_location,
    "_lucene_query": _lucene_query,
    "_fuzzy_titles": _fuzzy_titles
}

Here, I am importing anything my doctests might need, such as the Job model. Otherwise, I would have to clutter my doctests themselves with those imports. This would be a good place to do some setup common to the doctests, as well. __test__ is an undocumented dictionary that maps test name to callable object.

To run the tests, just execute the following command-line:

>python manage.py test
Creating test database...
...
Creating table search_job
...
Installing json fixture 'initial_data' from '../powerfill/search/fixtures'.
Installed 48 object(s) from 1 fixture(s)
...................
----------------------------------------------------------------------
Ran 19 tests in 2.285s

OK

As opposed to vanilla Python doctests, Django is actually spinning up a clean database, and populating your default fixture data. This is a great way to avoid having to mock models. Your continuous integration server could even run these tests without having a real database, by using sql lite.



I'm currently working at NerdWallet, a startup in San Francisco trying to bring clarity to all of life's financial decisions. We're hiring like crazy. Hit me up on Twitter, I would love to talk.

Follow @chase_seibert on Twitter