Speeding up Django tests during development.

This is part of outline created for a talk given at work regarding speeding up Django tests during development1.

We use Django extensively at work and some of my personal one off application also use Django because I am more productive in it.

If you are fan of TDD or work in fairly large enough web application with lots of developers, good test-suite or good documentation serves in reducing technical debt in the long run, since a corollary of writing testable code is you methods become more succinct and you would try to concentrate on business logic so as to have less dependency on on database while running test.

One of the things which hampers in TDD is if your test suite takes lot of time in running, following are some of things you can use while running tests to improve test running time.

Avoid IO

This seems obvious but try to avoid IO operations in tests either by mocking them or extracting business logic out and just testing that part.

Minimize IO Overhead

Avoiding IO altogether is not possible sometimes and when you might have inherited a legacy application you need a place to start,

to do this

Use mock Objects

Use a mock library like mock to install mock library if you are in python 22.

mock library makes mocking object really easy
eg.,

book = Mock(spec=Book)

creates a mock book object ready to be used in code.

Another good library to which immensely helps out while mocking models is model_mommy which provides fixtures for your models

eg.,

class Book(Model):  
    name = models.CharField(max_length=255)
    publisher = models.ForeignKey(Publisher)
    author = models.ForeignKey(Author)

when you do mommy.make(Book) it will automatically create a book object with arbitrary fields in compliance with constraint supplied3. also if you already have a Author object you can supply to Book model like mommy.make(Book, author=pb) here pb is a Publisher object.

This will save a query for Book model in the database, which might not sound much but when you have 100's of test-cases , it quickly adds up.

Use sqlite or MySQL in-memory mode

Following code changes database to SQLITE

if 'test' in sys.argv or 'test_coverage' in sys.argv: #Covers regular testing and django-coverage  
    DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'} 

ideally above code should also contain cache disabling which results in slight performance improvement but also is generally a good practice to avoid any surprises4.

if 'test' in sys.argv or 'test_coverage' in sys.argv: #Covers regular testing and django-coverage  
    DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'} 
    CACHES['default'] = {"BACKEND": 'django.core.cache.backends.dummy.DummyCache'}

this results in database being more performance, and since operations are happening in-memory, they are fast.

One last tip:

Change the order of Password hashers to a weaker hash which will result in considerable improvement in time.

if 'test' in sys.argv:  
    PASSWORD_HASHERS =(
        'django.contrib.auth.hashers.SHA1PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
        'django.contrib.auth.hashers.BCryptPasswordHasher',
        'django.contrib.auth.hashers.MD5PasswordHasher',
        'django.contrib.auth.hashers.CryptPasswordHasher', 
    )

If you are new testing or general TDD or are just learning django, I highly recommend Test-Driven Development with Python by Harry Percival.

Lastly I urge to run the tests once in a while with all these settings disabled because in essence this is mocking of actual environment and nothing really replaces the actual environment.

discuss this post on reddit

  1. researched from http://pyvideo.org/search?q=django+test

  2. this is already inbuilt in python 3

  3. For example, auto-generated name would not exceed 255 characters.

  4. The code uses dummy cache backend which acts like a blackhole - saves nothing, returns nothing.