Selenium’s been around for a long time now, and is available in various programming languages, but up until Django 1.4 came along you couldn’t have your Selenium tests (easily) integrated with your Django test suite. Since then, a new class named LiveServerTestCase, that your Selenium test classes can inherit from, was introduced. This class will automatically run a test server in the background and your Selenium tests will be run against that server. It not only simplifies the process, but it also means your tests can be setup the same way your Django tests are, and so both can be run together without any aditional setup using the standard
manage.py test command.
The best way to “understand” Selenium is by actually seeing it in action, so we prepared an example project with a small test for the Django Admin login page. To play with it, just create a new virtualenv and checkout our sample project on Github. The only dependencies are Django and Selenium, so a simple
pip install -r selenium_intro/requirements.pip inside your virtualenv will be enough.
To make things simple, we’ll use Firefox by default since it has webdriver support built-in. If you want to run tests on Chrome you’ll need to download the chrome driver separately from chromedriver downloads and add a
SELENIUM_WEBDRIVER variable in
settings.py to point to the Chrome driver instead (see
selenium_tests.webdriver in the sample project).
settings.py configuration serves two purposes:
- Not everyone is forced to run the Selenium tests, so it’s opt-in through
- You can switch to another webdriver without a big hassle. For production tests you can write a separate command and change the driver on the fly so you can test different browsers.
App / Test Structure
Selenium tests can be setup inside each app’s test directory just like a regular test would be, but since they’re very tied to the specific project, our preference is to create an app where all the tests are stored. We called this “selenium_tests” and other than the tests themselves, there are two other files:
Here we defined our base test class and helper methods. We have an “open” helper because Selenium needs an absolute URL (including server and port), but you can also define methods for common operations like “sign in”, “sign out”, etc.. that you may need throughout all the tests.
class SeleniumTestCase(LiveServerTestCase): """ A base test case for Selenium, providing hepler methods for generating clients and logging in profiles. """ def open(self, url): self.wd.get("%s%s" % (self.live_server_url, url))
Because the default webdriver query methods are very verbose (hello Java!) and missing some important features, we felt the need to create a custom webdriver class so we can add some functionality of our own.
Another option is to use splinter, which provides a much nicer Python interface to Selenium.
In this example, we have only one method, which is very handy:
find_cssis basically a shortcut for both “find_elements_by_css_selector” and “find_element_by_css_selector”, and as the method names imply, it enables you to find DOM elements using regular CSS selectors.
class CustomWebDriver(SELENIUM_WEBDRIVER): """Our own WebDriver with some helpers added""" def find_css(self, css_selector): """Shortcut to find elements by CSS. Returns either a list or singleton""" elems = self.find_elements_by_css_selector(css_selector) found = len(elems) if found == 1: return elems elif not elems: raise NoSuchElementException(css_selector) return elems def wait_for_css(self, css_selector, timeout=7): """ Shortcut for WebDriverWait""" return WebDriverWait(self, timeout).until(lambda driver : driver.find_css(css_selector))
As you start writing more and more tests, you’ll find yourself writing similar code to deal with selects or custom widgets for example. webdriver.py is where you can add these so your tests can be more DRY. As a rule of thumb, We tend to use
webdriver.py to add “generic” shortcuts and
test.py for “app specific” shortcuts (like sign in).
Writing your first test
Our example test will be a very simple Django admin sign in test, here’s how our
tests/auth.py looks like, and the inline documentation should be enough to get an understanding of how it works:
# Make sure your class inherits from your base class class Auth(SeleniumTestCase): def setUp(self): # setUp is where you setup call fixture creation scripts # and instantiate the WebDriver, which in turns loads up the browser. User.objects.create_superuser(username='admin', password='pw', email@example.com') # Instantiating the WebDriver will load your browser self.wd = CustomWebDriver() def tearDown(self): # Don't forget to call quit on your webdriver, so that # the browser is closed after the tests are ran self.wd.quit() # Just like Django tests, any method that is a Selenium test should # start with the "test_" prefix. def test_login(self): """ Django Admin login test """ # Open the admin index page self.open(reverse('admin:index')) # Selenium knows it has to wait for page loads (except for AJAX requests) # so we don't need to do anything about that, and can just # call find_css. Since we can chain methods, we can # call the built-in send_keys method right away to change the # value of the field self.wd.find_css('#id_username').send_keys("admin") # for the password, we can now just call find_css since we know the page # has been rendered self.wd.find_css("#id_password").send_keys('pw') # You're not limited to CSS selectors only, check # http://seleniumhq.org/docs/03_webdriver.html for # a more compreehensive documentation. self.wd.find_element_by_xpath('//input[@value="Log in"]').click() # Again, after submiting the form, we'll use the find_css helper # method and pass as a CSS selector, an id that will only exist # on the index page and not the login page self.wd.find_css("#content-main")
All that’s left is running your test:
./manage.py test selenium_tests