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.
