oebfare

Standalone Django App Testing

Yesterday I found myself hacking on a Django app. I am working on reviving my old django-categories app. More about that later. One thing I came across pretty quickly was the need to test the app. I had written tests way back when and wanted to ensure my new code worked with the API users were expecting. That is if there are actually any users, but mostly because this is the API I am wanting.

To test the app I basically needed a settings.py. I have a simple one living on my PYTHONPATH that I use regularly for running the Django test suite. However, it is not sufficient enough. It simply sets the DATABASE_ENGINE and ROOT_URLCONF settings. The latter setting is a requirement by Django. The requirement for running these tests involves having the app in INSTALLED_APPS which would defeat the purpose of the simple settings for running the test suite. I needed something to be a bit more robust.

Solving the problem

I was talking to Eric Holscher on IRC and I came up with the idea of having a standalone script that allowed some settings values to be passed in over the command line. The goal was to test the app. You can point it to the app and it will setup sys.path and INSTALLED_APPS for you. To point it at the app you would provide a path to the app:

app_test_runner.py /path/to/my/app

This is nice because you don't have to make sure the app lives on the PYTHONPATH allowing for quick testing. What you do need to ensure is that any dependancies the app has lives on the PYTHONPATH.

The other bit that is of importance is the ability to easily change some common Django settings. You can easily change the database settings right over the command line:

app_test_runner.py --DATABASE_ENGINE=postgresql_psycopg2 \
    --DATABASE_NAME=dbname \
    --DATABASE_USER=joe \
    --DATABASE_PASSWORD=password \
    /path/to/my/app

Pretty simple!

Give me some code

The code is available on GitHub. I did this so that it can continue to get better. The only thing I ask of future contributors is that it stays super simple. The script is pretty small so I would like to paste it here for convenience:

import os
import sys

from optparse import OptionParser

from django.conf import settings
from django.core.management import call_command

def main():
    """
    The entry point for the script. This script is fairly basic. Here is a
    quick example of how to use it::

        app_test_runner.py [path-to-app]

    You must have Django on the PYTHONPATH prior to running this script. This
    script basically will bootstrap a Django environment for you.

    By default this script with use SQLite and an in-memory database. If you
    are using Python 2.5 it will just work out of the box for you.
    """
    parser = OptionParser()
    parser.add_option("--DATABASE_ENGINE", dest="DATABASE_ENGINE", default="sqlite3")
    parser.add_option("--DATABASE_NAME", dest="DATABASE_NAME", default="")
    parser.add_option("--DATABASE_USER", dest="DATABASE_USER", default="")
    parser.add_option("--DATABASE_PASSWORD", dest="DATABASE_PASSWORD", default="")
    parser.add_option("--SITE_ID", dest="SITE_ID", type="int", default=1)

    options, args = parser.parse_args()

    # check for app in args
    try:
        app_path = args[0]
    except IndexError:
        print "You did not provide an app path."
        raise SystemExit
    else:
        if app_path.endswith("/"):
            app_path = app_path[:-1]
        parent_dir, app_name = os.path.split(app_path)
        sys.path.insert(0, parent_dir)

    settings.configure(**{
        "DATABASE_ENGINE": options.DATABASE_ENGINE,
        "DATABASE_NAME": options.DATABASE_NAME,
        "DATABASE_USER": options.DATABASE_USER,
        "DATABASE_PASSWORD": options.DATABASE_PASSWORD,
        "SITE_ID": options.SITE_ID,
        "ROOT_URLCONF": "",
        "TEMPLATE_LOADERS": (
            "django.template.loaders.filesystem.load_template_source",
            "django.template.loaders.app_directories.load_template_source",
        ),
        "TEMPLATE_DIRS": (
            os.path.join(os.path.dirname(__file__), "templates"),
        ),
        "INSTALLED_APPS": (
            # HACK: the admin app should *not* be required. Need to spend some
            # time looking into this. Django #8523 has a patch for this issue,
            # but was wrongly attached to that ticket. It should have its own
            # ticket.
            "django.contrib.admin",
            "django.contrib.auth",
            "django.contrib.contenttypes",
            "django.contrib.sessions",
            "django.contrib.sites",
            app_name,
        ),
    })
    call_command("test")

if __name__ == "__main__":
    main()

Known issues

One issue I came across while working on this bit of code was likely a bug in Django. In Django there is a unit test called ChangePasswordTest that uses setUp and tearDown to setup the TEMPLATE_DIRS to get the login.html template. Well, this overrides the whole TEMPLATE_DIRS and doesn't allow user defined values there. This creates a case where when django.contrib.admin is not in INSTALLED_APPS you will get test failures. I haven't had a terrible amount of time to look into this. I have temporarily patched this by including the admin app in INSTALLED_APPS.

Entry Details

Published: Nov 2, 2008 at 2:46 PM

© 2007 - 2008 Brian Rosner