StopFinder has a suite of unit and integration tests that probably has just about as much code as the application itself. The first thing I do after the deployment is run the tests. This is in the nature of a smoke test, to make sure that I haven’t messed up syncing with third-party libraries and such. If the tests pass on my local machine, I don’t really expect them to fail when they’re deployed to another host.

While it’s great to run the integration tests on the same database configuration that the app itself will be running on, sometimes your host environment will make that impossible. I deploy Django sites in a lot of different places. Often times, the hosts that I’m working with aren’t sitting in my apartment under my direct control, but are rather owned by folks who make their living by maintaining app environments for people like me.

The Django unittest framework operates by creating a test database at the beginning of every run, and then wiping the tables after each test. At the end of the entire run, the database is dropped. This can cause some problems if you don’t have control of your host. For example, some shared hosting plans will create a single database for you, and disallow you from issuing CREATE DATABASE or DROP DATABASE queries yourself. (My WebFaction account operates like this, for example.)

If you’re in the hosting scenario I just described, you can’t run your tests without hand-editing your database properties, or using a different testing-specific settings file, or something else cludgy like this. This annoyed me when I first encountered it, but I was pretty swamped at the time. I budgeted about 10 minutes to resolving the problem, and came up with this very simple solution.

The idea is to wrap django-admin.py runtests so that I can select the database configuration of my choice. If I don’t have permission to create and drop databases, I’ll use SQLite as the test database. Otherwise, I’ll use the resident database server itself.

Here’s how to do that in two easy steps.

1. Write a script to drive django-admin.py runtests that sets an environment variable when you want to use SQLite (which I stupidly called a “test database” flag). The script then issues a shell call to django-admin:

import sys

_TESTING_COMMAND = 'django-admin.py test'

def run(args):
    """
    Just runs the tests, with the caveat that we've reset the environment
    variable to something we want. If you want to use test database settings,
    specify -tdb.
    """

    if len(args) == 1 and args[0] == "-tdb":
            os.environ["TESTING_DJANGO"] = "THIS_VALUE_DOESN'T_MATTER"

    error_code = os.system(_TESTING_COMMAND)

    if error_code != 0:
        print "Testing command failed: errcode %d" % error_code

if __name__ == "__main__":
    run(sys.argv[1:])

2. In your local settings files, or wherever else you define your database settings, you can override the production values by doing something like:

if "TESTING_DJANGO" in os.environ:
    DATABASE_ENGINE = 'sqlite3'
    DATABASE_NAME = ':memory:'
    DATABASE_USER = ''
    DATABASE_PASSWORD = ''
    DATABASE_HOST = ''
    DATABASE_PORT = ''
    TEST_DATABASE_NAME = ":memory:"

Alternately, you could have the SQLite settings be the default, and then in the local settings file you could decide to bind the database-related properties to the production database settings only if the TESTING_DJANGO flag is not set. Whatever works best for you; this is just the general idea.

Yes, this is sort of hacky, but I had it up and running in a handful of moments and it does frequently come in handy. I was hoping that by running the tests on an in-memory database it would also speed things up a bit, but I guess the table creation and data-loading time overshadows the actual time spent exercising the database in test code; running the StopFinder tests under SQLite actually takes just as long or longer than when I run it against postgres.

Hope this helps someone out there! For all I know, more recent Django builds have this sort of flexibility directly baked-in.

5 Responses to “Flexible test-database engine selection in Django”

  1. sean Says:

    I’m trying to do some tests in different environments these days,
    so it’s a quite useful tip to me,
    thanks a lot.

  2. norm Says:

    put your test db settings into a file called test_settings.py

    in test_settings.py add this line to the top

    from settings.py import *
    #database settings/other test settings.

    then do this
    python manage.py test –settings=test_settings

  3. Bryan Says:

    Excellent concept! I really like norm’s approach here also. I think this is relevant, whether you have special environment considerations or not. Thanks!

  4. Pete Says:

    I found this article very useful! Norm’s suggestion was the icing on the cake. Tests that were running with mysql in over 9 seconds now run in just over 0.1 seconds with sqlite3. Thank you!

  5. Sam Says:

    This is what I do to always run tests in memory:

    if ‘test’ in sys.argv:
    # test database settings go here.

    I have this in my local_settings.py that’s imported at the bottom of settings:

    try:
    from local_settings import *
    except ImportError:
    pass

    In my build that actually tests on postgres I use the ‘–settings=test_settings_postgres’ and have a seperate settings file that overwrites the sqllite paramters.

    Hope that helps,
    Sam

Leave a Reply