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