If you’ve never heard of Selenium, put simply, it’s a tool that allows you to create tests that are run in the browser and interact with your UI in the same way as if you were manually testing your website or app. It’s the de-facto standard to test complex Web UI interactions that usually involve a heavy use of JavaScript, and that’s probably the main use-case for it. Other than that, we also use it sometimes as a helper tool for cross-browser design (CSS) testing by running Selenium tests through different browsers and taking screenshots or recording videos.

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.

Installation

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.

Settings

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).

Our settings.py configuration serves two purposes:

  1. Not everyone is forced to run the Selenium tests, so it’s opt-in through INSTALLED_APPS.
  2. 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:

test.py

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))

webdriver.py

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_css is 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.
  • wait_for_css is a helpful method to block the test from progressing until a element (css selector) is found on the page. This method is commonly used in non-blocking instructions (such as AJAX requests) or some JavaScript interactions that do DOM manipulation. Whenever you get an “element not found” error due to JavaScript being slower than your test instructions, this is the method to use.
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[0]
        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='info@lincolnloop.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

Happy testing!