jonathan.stoppani.name

Combining py.test and Selenium

I recently started using py.test for the unit testing of a couple of in-house projects. I particularly fell in love with their concept of fixtures and overall cleanliness of the resulting test code, so wondered if I could use it alongside with Selenium for the functional testing part as well.

Thanks to a custom fixture, the integration of these two libraries was straightforward. The code below shows how to setup a browser fixture to connect to a Selenium Remote driver for each test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# conftest.py

import pytest
from selenium import webdriver


BROWSERS = {
    'firefox': DesiredCapabilities.FIREFOX,
    'chrome': DesiredCapabilities.CHROME,
}

WEBDRIVER_ENDPOINT = 'http://localhost:4444/wd/hub'


@pytest.yield_fixture(params=BROWSERS.keys())
def browser(request):
    driver = webdriver.Remote(
        command_executor=WEBDRIVER_ENDPOINT,
        desired_capabilities=BROWSERS[request.param]
    )
    yield driver
    driver.quit()

Once familiar with the way py.test is organized, using this fixture comes down to putting the above code in a file named conftest.py at the appropriate location and using the fixture from one or more test cases by declaring a test function with a named browser argument:

1
2
3
4
5
# test_functional.py

def test_python_org(browser):
    browser.get('http://python.org')
    assert 'Python' in browser.title

Bonus: DRY URLs

As I found myself testing against a development instance of the web application, I had plenty of calls to browser.get(...) with different development URLs. These URLs, and particularly the domain:port portion change depending on the environment you are testing against (dev, test, staging,…) and I wanted an easy way to adapt my test code to it.

The solution I came up with consists in extending the webdriver.Remote class and providing my own implementation of the get method, which joins the provided URL with one defined at instantiation time by the calling code.

The resulting class and an example of its usage are shown in the next code snippet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import pytest
from urlparse import
from selenium import webdriver


BROWSERS = {
    'firefox': DesiredCapabilities.FIREFOX,
    'chrome': DesiredCapabilities.CHROME,
}

WEBDRIVER_ENDPOINT = 'http://localhost:4444/wd/hub'

BASE_URL = 'http://python.org'


class BaseUrlWrapper(webdriver.Remote):
    def __init__(self, base, *args, **kwargs):
        self._base_url = base
        super(BaseUrlWrapper, self).__init__(*args, **kwargs)

    def get(self, url):
        url = urljoin(self._base_url, url)
        return super(HostMappedWrapper, self).get(url)


@pytest.yield_fixture(params=BROWSERS.keys())
def browser(request):
    driver = BaseUrlWrapper(
        base=BASE_URL,
        command_executor=WEBDRIVER_ENDPOINT,
        desired_capabilities=BROWSERS[request.param]
    )
    yield driver
    driver.quit()


def test_homepage(browser):
    browser.get('/')
    assert 'Python' in browser.title


def test_about(browser):
    browser.get('/about/')
    assert 'About Python' in browser.title


def test_other(browser):
    # Absolute URLs still yield the expected result
    browser.get('https://www.ruby-lang.org/en/')
    pytest.fail("Ruby always fails!")

Possible optimizations

The code presented above is not the most performant version, as a new instance of each browser is spawned for each test and closed right after it.

An approach which could yield faster test execution times would be to open each browser just once per testing session and then just clear the browsing session each time. Some hints for such an implementation can be found in the Selenium API documentation. Once I had the chance to try them out, I may write a follow-up post.

Filed under