Month: January 2016

Acceptance testing database hack in Django

A couple of months ago, I started working on a web application project with a lot of acceptance tests. Because the project was two years old, everything I expected to do was to add some new features and keep the project running.

So there are a lot of techniques that can be applied in order to shorten running time – people try to optimize their code and use Sqlite in memory.

It’s been a while since I develop web applications using Django. This is clear that testing is one of the most important parts of the development process. And everybody knows how time-consuming it is to run those tests either unittests or acceptance ones over and over again.

So I what I am going to here is to show how I tried to make tests running time even shorter and I assume that your project is setup and ready to go. In this post, I am going to use Lettuce testing framework for testing.

The prerequisites to this hack:

  1. Django project.
  2. Lettuce testing framework installed and fully functioning (or any other like Behave).
  3. Lettuce’s file (or any other file that contains test runner configurations)
  4. Behavioral (acceptance) tests already written using Lettuce.

So when you run:

./ harvest

Django creates a new database for you and runs all acceptance tests in the system. Everything is good while you do not have that much acceptance tests, but what if your project is huge and it has up to two thousand testing scenarios already written and you have somehow figure out how to make running time shorter.

The first thing what comes to mind is to keep testing database in memory and you probably added that line ‘memory’ config in your testing database settings.

    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ":memory:",

This is what Django’s official documentation offers. Now, let us try to redo this in a different way.

So here is the list of actions I am going to do:

  1. create a RAM memory storate directory
  2. place our sqlite db to that directory
  3. modify the Lettuce’s file to efficiently handle sqlite file.

Create a RAM storage directory

For this purposes run the following commands:

sudo mkdir /media/ramdisk # it will create a ramdisk directory
sudo mount -t tmpfs -o size=500M,mode=0777 tmpfs /media/ramdisk # mount to RAM
sudo gedit /etc/fstab
tmpfs /media/ramdisk tmpfs size=500M,mode=777 0 0

The last two lines save changes so that ramdisk directory is available even after system reboot.

Now we can change our database settings shown above:

    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': "/media/ramdisk/mytest_db.sqlite3",

So, if I run tests with this settings, Django will create mytest_db.sqlite3 database in RAM. But if we leave it without further actions, this would not be clever.

The next step is to add a few lines of code into our file.

I usually call it from file’s function that run before each scenario. The code is below:

def handle_db_recovery():
    db_name = settings.DATABASES['default']['NAME']
    db_copy = db_name + 'template'
        if os.path.isfile(db_copy):
            call_command('migrate', interactive=False, verbosity=1)
            shutil.copyfile(db_copy, db_name)
    except OSError:
        pass # this is not good actually, but I didn't found a better way
    call_command('migrate', interactive=False, verbosity=1)
    shutil.copyfile(db_name, db_copy)

So, let us look what the snippet is actually doing. The db_name is the name of database from settings files that you use and db_copy is the copy of original database plus concatenation of ‘template’ to the end of the name.

Then we check whether there is a database with the db_copy name in ramdisk directory, which is in RAM. If yes, simply copy that database into original one and if not, create a new database from scratch.

What it gives is that when you test a web application, you need your test to be independent from each other. So the first step to achieve that is to clean the database after every testing scenario.

The code snippet serves exactly that purpose. If you call the function before every testing scenario run, it will save your time and will not create a new db or run migrations and etc, but simply keep a copy of ready-to-use database file for you.

But keep in mind that if you have some new migrations, you need to go to ramdisk directory and manually delete that copy sqlite file and rerun your tests. Or you would better add a code snippet that can handle such situations for you.

Good luck!