Recently I started adding unit and acceptance tests to a webapp using Selenium, integrated into the existing py.test framework that tests the backend code.
py.test fixtures make using Selenium, via its Python bindings, really straightforward. Here’s how I did it.
First I put all the Selenium related tests in a tests/selenium/ directory. I then created tests/selenium/conftest.py and wrote a fixture to allow tests to access a single instance of the webdriver for the entire session:
import pytest
from selenium import webdriver
browsers = {
'firefox': webdriver.Firefox,
'chrome': webdriver.Chrome,
}
@pytest.fixture(scope='session',
params=browsers.keys())
def driver(request):
if 'DISPLAY' not in os.environ:
pytest.skip('Test requires display server (export DISPLAY)')
b = browsers[request.param]()
request.addfinalizer(lambda *args: b.quit())
return b
Note that we’re able to parameterise the fixture so that it runs with multiple browsers. We then add a per-function fixture that sets up the session for an individual test:
@pytest.fixture
def b(driver, url):
b = driver
b.set_window_size(1200, 800)
b.get(url)
return b
A fixture can refer to other fixtures of more generic scope. So url is a fixture that accesses the optional --url property.
def pytest_addoption(parser):
parser.addoption('--url', action='store',
default='http://localhost/portal/portal.html')
@pytest.fixture(scope='session')
def url(request):
return request.config.option.url
These fixtures are available for all tests in that package. Tests have the form:
def test_badger(b):
# test goes here
We can also create per-module fixtures, that optionally inherit our generic fixtures. Say for example we want to run a number of tests (e.g. for WCAG 2.0 compliance) on a number of parameterised instances of the set-up webapp. We might do this in test_wcag.py:
import pytest
@pytest.fixture(scope='module')
def wcag(driver, url):
"""
Set up a single session for these tests.
"""
b = driver
b.set_window_size(1200, 800)
b.get(url)
# do stuff here with Selenium to set up webapp
return b
We can now write tests ((find_elements_by_jquery() is a method I’ve added in an extension of Selenium’s webdriver, and is a topic for another post.)) in this module, e.g.
@pytest.mark.wcagF17
@pytest.mark.wcagF62
@pytest.mark.wcagF77
def test_unique_ids(wcag):
"""
All ids in the document should be unique.
"""
elems = wcag.find_elements_by_jquery('[id]')
ids = map(lambda e: e.get_attribute('id'), elems)
assert len(elems) >= 1 # sanity check
assert util.unique(ids)
Again, we can parameterise this fixture to set up the webapp in a number of different ways. Note that we have to use driver as our fixture, not b. This is because we can only refer to fixtures more general in scope than the one we are writing.