Technology for Dogs

PhantomJS, Selenium, and Django: Headless Browser Testing for the Rest of Us

Rover.com is a Django shop, but I personally come from a Rails background. The Rails world has great tooling and infrastructure for automated functional tests – capybara, capybara-webkit, and the new hotness poltergist. Underneath poltergeist lies PhantomJS, a headless webkit with very few dependencies, excellent for automated testing. Unfortuantely, PhantomJS version 1.5 dropped Python bindings, leaving us Djangonauts out to dry. There also isn’t a great capybara equivalent in the Python world (Ghost.py is the closest).

Thankfully, despite the roadblocks, there is a path forward! Django (starting with 1.4) comes with LiveServerTestCase to support our exact use case. After failed attempts to get Ghost.py up and running (due to dependencies and lack of documentation), I landed upon a solution that will start us Django developers down the path of being first class citizens once more (well, maybe not exactly first class, but at least those coach seats up front with a little extra legroom). I’ve also taken some baby steps to improve our testing assertion syntax, trying to fill the capybara void, which I’ll get into at the end of this post.

Let’s get started.

Install the Prerequisites

  • NodeJS
  • Fontconfig
  • PhantomJS
  • Selenium

The following steps are for Ubuntu 12.04 LTS.

NodeJS

Ubuntu 12.04’s default ppa’s don’t have the latest version of NodeJS, so we first need to add a new repository.

1
2
3
4
5
sudo apt-get install software-properties-common
sudo apt-get update
sudo apt-get install -y python-software-properties python g++ make
sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update

Now, we can install

1
sudo apt-get install nodejs

Fontconfig

For use with PhantomJS

1
sudo apt-get install fontconfig

PhantomJS

NodeJS comes with npm, their package manager. From that, we can install PhantomJS.

1
sudo npm -g install phantomjs

Note here that I’m installing it globally, which is not required to get this to work, but can make things easier.

Selenium

1
pip install -U selenium

Application setup

With our system set up to support our automated testing, we now need to set up our application so we can start writing our own tests.

Add selenium to your INSTALLED_APPS

1
2
3
4
5
INSTALLED_APPS = (
    'django.contrib.auth',
    '...',
    'selenium',
)

Install the Selenium Utility Belt

We’ve written and open-sourced the very beginning of what we hope will grow into a more fully-featured assertion framework Selenium testing in Django. We created this to wrap some of the selenium syntax into a more expressive feature set to enable more rapid test writing. When testing interfaces, it is really nice to be able to express assertions as they relate to your goals, such as assertOnPage. This was the impetus for creating the selenium utility belt. The project lives at https://github.com/roverdotcom/selenium-utility-belt and is in its infancy. We hope to add to this (with your help!), rework its structure, and release it onto PyPI for wider consumption.

Copy the selenium_utility_belt.py file into your project at a convenient place to import it to your base test case class. We toss ours in our common app, in the test folder.

Base class for your test cases

1
2
3
4
5
6
7
from django.test import LiveServerTestCase
from common.test.selenium_utility_belt import SeleniumUtilityBelt


class InterfaceTestCase(SeleniumUtilityBelt, LiveServerTestCase):
    def setUp(self):
        super(InterfaceTestCase, self).setUp()

Write your first test!

With just a little bit of wrapping of the Selenium API, our interface becomes very simple to test. Here we have a single test that asserts the presence and visibility of our Location text entry field on our homepage.

1
2
3
4
5
6
7
8
9
10
from common.test import InterfaceTestCase


class HomepageTests(InterfaceTestCase):
    def setUp(self):
        super(HomepageTests, self).setUp()

    def test_location_field_on_homepage(self):
        self.open('/')
        self.assertOnPage('#location', visible=True)

Gotchyas

Port collisions

When running these tests on our CI server, we ran into the issue of port collisions for the running PhantomJS processes. LiveServerTestCase had foresight on this issue, and added an option when you run your tests. Simply specify the range of ports to use when you call your test runner.

1
./manage.py test --liveserver=localhost:8090-8100

Recent versions of django-jenkins take this option as well, so you can easily use these tests on your CI server

Phew!

If you have run into any questions or hit any roadblocks along your way here, you can reach me at @croby. Contributions to the utility belt are welcome and encouraged!