Integration Test with Python Selenium

April 8, 2018


Integration, or end to end (E2E) test, test the entire utility of your app in the same go. This is different than unit test, which test units of your code, either in the back or front end. For example, it is common to use selenium to automate a browser to have a staged frontend server interface with a staged backend server and assert certain conditions.

I've typically written integration test in Javascript (WebdriverIO is my favorite for the record!), but recently I had to set up a test suite which uses Python. It was a lot of fun, but not necessarily straightforward to do from the start. Here is my complete setup that I ended up with. I'll put down everything including how I run the test runner so as to leave nothing unanswered.

My directory structure looks something like this:

/integration_test
  __init__.py
  /module1
    __init__.py
    test_feature1.py
  /module2
  /pageobjects
    __init__.py
    login_page.py
    module_page.py
  test.py
  /utils
    __init__.py
    selenium_test_case.py

Generic test setup (skip if you want selenium goodness only)

First thing is first - I needed a test runner in Python to run my integration test. Fairly obviously, I chose to use the built in unittest module. My preferred way to do this is to make an explicit list of classes to run, so this is what my setup ended up looking like. There is a lot to this library, so I highly recommend giving this a read if you haven't built test from the ground up yourself already

# Chromedriver binary puts chromedriver on the PATH environment variable
# so we don't need to start our own selenium server.. Nice!!
import chromedriver_binary
from unittest import TestLoader, TextTestRunner, TestSuite

from module1.test_feature1 import TestLogin
from module1.test_feature2 import TestNavigation

# Group test classes to be run
test_classes = [
    TestLogin,
    TestNavigation,
]

if __name__ == "__main__":
    # initialize test objects
    loader = TestLoader()
    runner = TextTestRunner()
    # create loaded test instances
    suites_list = [loader.loadTestsFromTestCase(test_class) for test_class in test_classes]
    allSuites = TestSuite(suites_list)
    # execute test
    runner.run(allSuites)

Page object pattern

The page object pattern is the concept of abstracting your commands used to run test via selenium out of your immediate testing code. It might seem like a waste of time at first, but it keeps code DRY and the test are much more readable. I have had great success using this pattern on an integration test suite that spanned hundreds of test. Once the page objects were built out, writing test becomes a breeze.

Rather than explain it, lets look at a couple example page objects I might write in Python, and what the corresponding test would look like.

#/pageobjects/login_page.py

from page_objects import PageObject, PageElement
from selenium import webdriver
from settings import LOGIN_EMAIL, LOGIN_PASSWORD

class LoginPage(PageObject):
    username = PageElement(css="#username")
    password = PageElement(css="#password")
    forgot_password = PageElement(css="span.forgot-password")
    login = PageElement(css="div.button")

    def log_into_app(self):
        self.logo.find_element #add assertion here
        self.username.send_keys(LOGIN_EMAIL)
        self.password.send_keys(LOGIN_PASSWORD)
        self.login.click()


# /pageobjects/module_page.py

# Nifty little library I found which simplifies using selenium
from page_objects import PageObject, PageElement
from selenium import webdriver

class ModulePage(PageObject):
    nav_link = PageElement(css="#sidebar .section [href='#/module-1']")
    title = PageElement(css=".app-header h1")  # generic css selector
    def __init__(self, driver):
        super(ModulePage, self).__init__(driver)
        self.driver = driver

    def get_list_item(self, index):
        """ Including this so it is more clear why you might want to initialize this object with the driver
            available in the page object """
        elements = self.driver.find_elements_by_css_selector(".module-container .my-list")
        if index > len(elements) - 1:
            raise Exception('Index is too high for the number of elements found')
        else:
            return elements[index]

Creating a selenium wrapper around test case

One very annoying issue I ran into (and the reason I created this class in the first place) was that after each test method, I had to re-log into my app every time. This is far from ideal for many reasons, but to name a couple: 1) It greatly slows down your test. 2) Discourages each test method actually testing an individual feature. What I wanted was to start writing test after the login, where I could focus on testing features and not re-logging into my app constantly. Below is my answer to that.

What I'm actually doing is opening up the browser and logging in within setUpClass, and on the class teardown (tearDownClass) I close the driver. Now as long as my test classes inherit from this class, I don't need to worry about doing either of those anymore! Note that I do not override the __init__ method here. The init method actually gets executed for every test method that gets run. Also note that I'm inheriting from TestCase so that my SeleniumTestCase class plays nicely with Python's unittest module.

from unittest import TestCase, skip
from selenium import webdriver
from page_objects import PageObject, PageElement

from pageobjects.login_page import LoginPage
from settings import HOST

class SeleniumTestCase(TestCase):
    """
    A wrapper of TestCase which will launch a selenium server, login, and add
    cookies in the setUp phase of each test.
    """
    @classmethod
    def setUpClass(cls, *args, **kwargs):
        cls.driver = webdriver.Chrome(port=4444)
        cls.driver.implicitly_wait(15)
        cls.driver.get(HOST)
        # page obect I wrote which accepts the driver and can login to my app
        cls.login = LoginPage(cls.driver)
        cls.login.log_into_app()

    @classmethod
    def tearDownClass(cls):
        cls.driver.close()

Bringing it all together

Now that we have a selenium wrapper of TestCase which will log in for us and hold a session throughout each test method, and we have a couple page objects, lets wee what our test will look like.

# /module1/test_feature1.py
from unittest import skip
from utils.selenium_test_case import SeleniumTestCase
from pageobjects.module_page import ModulePage
from settings import HOST

class NavTest(SeleniumTestCase):

    def setUp(self):
        super(NavTest, self).setUp()
        # Initialize page objects for each test
        self.module = ModulePage(self.driver)
        # launch page
        self.driver.get(HOST)

    def test_feature1self):
        self.module.nav_link.click()
        self.assertEqual(self.module.title.text, 'Module Title')

    @skip('unimplemented test')
    def test_feature2(self):
        pass

Nice and clean! Please leave a comment if I was unclear about any particular steps. Thanks for reading.

Here are some of the key requirements I used for those interested

/requirements.txt
chromedriver-binary==2.35.0
page-objects==1.1.0
selenium==3.8.1
Comment Enter a new comment:

On April 8, 2018 david.brady wrote: Reply

Thanks Nick. This is a very useful and relevant blog. Test driven development has grown in popularity over the years but that is only useful for unit testing. However, there is a real need for more robust end to end integration testing as systems become more interdependent and deployed across multiple platforms.