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.
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
sudo apt-get install nodejs
Fontconfig
For use with PhantomJS
sudo apt-get install fontconfig
PhantomJS
NodeJS comes with npm, their package manager. From that, we can install PhantomJS.
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
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
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
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.
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.
./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!