RkBlog

Hardware, programming and astronomy tutorials and reviews.

Testing Django applications with Selenium

Testing applications and their frontend with an automated browser

Selenium is a popular tool for browser automation. Main usage for Selenium are browser based tests of web applications. With a real browser and handy API you can test frontend behavior of an application (like JavaScript code).

In Django 1.4 we got LiveServerTestCase - a TestCase that spawns a test server. This allowed much easier Selenium integration in Django tests. You can change a presentation on benlopatin.com. If you will find something different - check the date as it may describe older solutions for older Django versions.

In this article I'll show some basic Selenium usage cases in tests - showcasing the api.

Basics

At start install Python selenium package:
pip install selenium
After that you can start writing tests. The core looks like so:
from django import test
from selenium.webdriver.firefox import webdriver

class BasicTestWithSelenium(test.LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        cls.selenium = webdriver.WebDriver()
        super(BasicTestWithSelenium, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        super(BasicTestWithSelenium, cls).tearDownClass()
        cls.selenium.quit()

Methods of this class can use self.selenium to operate via Selenium on a browser (in this case Firefox). You can open URLs, find elements on page, fill and submit forms and more. Aside of mentioned presentation you can look at the sources or documentation for a list of API methods available.

A simple Selenium test checking if "Django administration" header is displayed on /admin/ URL would look like so:
    def test_if_admin_panel_is_displayed(self):
        url = urljoin(self.live_server_url, '/admin/')
        self.selenium.get(url)
        header = self.selenium.find_element_by_id('site-name').text
        self.assertEqual('Django administration', header)
The H1 header of the admin panel has an ID "site-name" so I can use "find_element_by_id" to get the element. This test is simple, doable also via standard test engine, but in real tests Selenium will usually tests something not testable with standard TestCase.

Testing Facebook authentication with Selenium

How to test a Facebook authentication managed by JavaScript SDK? Here is a template (Django view) with the authentication code:
<!DOCTYPE html>
<html>
<head>
<title>FB Login</title>
<meta charset="utf-8" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
</head>
<body>
    <div id="fb-root"></div>
    <script>
      window.fbAsyncInit = function() {
        FB.init({
          appId      : 'YOUR_FACEBOOK_APP_ID', // App ID
          status     : true, // check login status
          cookie     : true, // enable cookies to allow the server to access the session
          xfbml      : true  // parse XFBML
        });
        // Additional initialization code here
      };
      // Load the SDK Asynchronously
      (function(d){
         var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
         if (d.getElementById(id)) {return;}
         js = d.createElement('script'); js.id = id; js.async = true;
         js.src = "//connect.facebook.net/en_US/all.js";
         ref.parentNode.insertBefore(js, ref);
       }(document));

        $(document).ready(function() {
            $('#login').click(function() {
                FB.login(function(response) {
                    if (response.authResponse) {
                        $('.status-bar').html($('.status-bar').html() + '<p>FB connected</p>');
                         FB.api('/me', function(response) {
                            $('.status-bar').html($('.status-bar').html() + '<p>' + response.name + '</p>');
                        });
                    } else {
                        $('.status-bar').html($('.status-bar').html() + '<p>Not connected to FB</p>');
                    }
                }, {scope: ''});
            });
        });
    </script>
<a href="#" id="login"><b>Facebook Login</b></a><br />
<div class="status-bar"></div>
</body>
</html>

So we have a "Facebook Login" link, that when clicked will open a new window with a Facebook login widget. If user logs in the JavaScript API will query for user name and print it on the page. For this code to work you have to set application ID of you Facebook App.

To test this login process with selenium we will need a test Facebook user and one change in used Facebook app config. The "Site URL" (Website with Facebook Login) needs to be set to "http://localhost:8081/" - which is the default URL of the dev server during tests. As the test user you have to autorize the application prior to running selenium tests (so that they get only the login form and not extra authorization form).

The test looks like so:
import time
from urlparse import urljoin

from django import test
from selenium.webdriver.firefox import webdriver

class TestFacebookLoginWithSelenium(test.LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        cls.selenium = webdriver.WebDriver()
        super(TestFacebookLoginWithSelenium, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        super(TestFacebookLoginWithSelenium, cls).tearDownClass()
        cls.selenium.quit()

    def test_if_user_logs_in(self):
        facebook_name = 'TEST_ACCOUNT_USER_NAME'
        self.open_login_page()
        windows = self.selenium.window_handles
        self.selenium.switch_to_window(windows[1])
        self.submit_login_form()
        self.selenium.switch_to_window(windows[0])
        time.sleep(5)
        response = self.selenium.find_element_by_css_selector('body').text
        self.assertTrue(facebook_name in response)

    def open_login_page(self):
        url = urljoin(self.live_server_url, '/test/')
        self.selenium.get(url)
        login_link = self.selenium.find_element_by_id('login')
        login_link.click()

    def submit_login_form(self):
        facebook_email = 'TEST_ACCOUNT_EMAIL'
        facebook_pass = 'TEST_ACCOUNT_PASSWORD'

        email = self.selenium.find_element_by_name('email')
        password = self.selenium.find_element_by_name('pass')
        email.send_keys(facebook_email)
        password.send_keys(facebook_pass)
        submit = 'input[type="submit"]'
        submit = self.selenium.find_element_by_css_selector(submit)
        submit.click()

We have a test_if_user_logs_in test and two helper methods. First method clicks in the login link, the second fills and submits the login form. As Facebook login opens a new window we have to switch between windows with "window_handles" (list of available windows) and "switch_to_window". Sleep function is there to wait for the Facebook asynchronous JavaScript code to execute (the page is loaded so selenium wouldn't wait).

open_login_page opens given url (self.selenium.get) and then finds a link given by ID on that opened page (self.selenium.find_element_by_id). The link is clicked which opens the login window.

submit_login_form finds form fields (find_element_by_name) and fills them with data (send_keys) - email and password. In the end it finds a submit type button and clicks it submitting the form.

When something is not there

Selenium find methods will raise NoSuchElementException if they can't find given element. That can also be used for tests checking if something is not there. Just import the exceptions:
from selenium.common import exceptions
And use that in tests, like:
    def test_if_page_doest_contains_a_tab(self):
        url = urljoin(self.live_server_url, '/test/')
        self.selenium.get(url)
        def find_tab():
            self.selenium.find_element_by_id('tab')
        self.assertRaises(exceptions.NoSuchElementException, find_tab)

That's how testing with Selenium looks like, at least in the basic form. This tool can also be used for acceptance tests checking user flow on the website or for task automation on external websites (login, check for data or perform some actions).

RkBlog

Django web framework tutorials, 6 August 2012,

Comment article