Playing with LiveServerTestCase and Selenium

At "Djangocon Tolosa", Julien phalip spoke about a new feature in django 1.4, LiveServerTestCase. A LiveServerTestCase launches a true HTTP server and gives a way to make HTTP requests in test cases. It can be useful to test REST API over a full HTTP protocol stack. Or, associated with Selenium, it can be powerful to make functionnal tests, on a view using ajax for example.

Let's try it with a simple usecase : * a view which displays 10 first results of a dummy search in a <table> * a view which returns dynamically the 5 next results * a button wich launches an ajax request on this second view to fetch 5 more results and append it to the current list

Here is a simple code extract of each part :

# urls.py
...
url(regex=r'^results_page$',
    view='results_page',
    name='results_page'),
url(regex=r'^get_more_lines$',
    view='get_more_lines',
    name='get_more_lines'),
...

# views.py
...
def results_page:
    """ Renders a page with result list in a <table> """

def get_more_lines:
    """ Returns dynamically 5 more lines to be displayed in table """
...
# template.html
...
<table id="results">
  {% for r in results %}
    <tr><td>{{ r }}</td></tr>
  {% endfor %}
</table>
<a id="more-lines" href="#">Get more lines</a>
...
// scripts.js
...
$('#more-lines').click(function(e){
  $.ajax({
    url: '/get_more_lines',
    success: function(response){
      if(response) {
        $('#results').append(response);
      } else {
        $('#more-lines').addClass('disabled');
      }
    }
  });
});
...

Note : code above is incomplete but interesting part is below.

Now we want to test it little bit. For example :

  • Ensure that table contains 10 lines at begining
  • Ensure that table contains 15 lines after click on "Get more lines" button
  • Ensure that button becomes disabled if there is no more result

It's not really a unit test for js function or django view. Looks like more a quick functional test.

Here is the corresponding LiveServerTestCase :

from selenium.webdriver.firefox.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from django.utils.unittest import SkipTest
from django.test import LiveServerTestCase
from django.core.urlresolvers import reverse

class ResultListTestCase(LiveServerTestCase):

    @classmethod
    def setUpClass(cls):
        """ Instantiate selenium driver instance """
        cls.selenium = WebDriver()
        super(ResultListTestCase, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        """ Quit selenium driver instance """
        cls.selenium.quit()
        super(BaseSeleniumWebDriverTestCase, cls).tearDownClass()

    def _wait_ajax_complete(self):
        """ Wait until ajax request is completed """
        WebDriverWait(self.selenium, 10).until(
            lambda s: s.execute_script("return jQuery.active == 0")
        )

    def _has_css_class(self, selector, klass):
        """
        Returns True if the element identified by `selector`
        has the CSS class : `klass`.
        """
    return (self.selenium.find_element_by_css_selector(selector)
            .get_attribute('class').find(klass) != -1)


    def test_get_more_lines(self):
        """ Test result list and 'get more lines' button """

        # Display tested page
        url = reverse('results_page')
        self.selenium.get(self.live_server_url + url)

        # Ensure 10 lines are displayed
        rows_length = lambda: len(self.selenium.find_elements_by_css_selector('#results tr'))
        self.assertEqual(rows_length(), 10)

        # Click on 'get-more-lines' button
        self.selenium.find_element_by_id('get-more-lines').click()
        self.wait_ajax_complete()
        self.assertEqual(rows_length(), 15)

        # Click again and check button is disabled
        self.selenium.find_element_by_id('get-more-lines').click()
        self.wait_ajax_complete()
        disabled = self.has_css_class('#increase-history', 'disabled')
        self.assertTrue(disabled)

On my current project, tests are ran by Jenkins on a headless server, so Selenium can't launch a firefox. Awaiting for a specific configuration, I wrapped creation of WebDriver in a try/except like this :

class ResultListTestCase(LiveServerTestCase):

    @classmethod
    def setUpClass(cls):
        try:
            cls.selenium = WebDriver()
            super(ResultListTestCase, cls).setUpClass()
        except Exception as e:
            raise SkipTest('Selenium webdriver is not operational')

This is just a really simple first test but this feature seems pretty cool IMHO :-).

Comments !