Structuring Selenium Test Code

I had the opportunity to use the Selenium browser automation suite for a project at my, now previous, job. Our testing needs weren't anything spectacular, especially considering we had absolutely zero in the way of automated testing for the project I was working on. I figured anything now would just be icing on the cake.

</p>

Playing around with Selenium, the very first thing I struggled with was how to structure my test code. I did a bit of research and a bit of my own architecture to produce a solution that worked well for me. Your mileage may vary.

</p>

Page Objects

</p>

The first major revelation for me was the idea of structuring each web page as an object. Each of these page objects should return another page object during an operation. Sometimes it might be the same page, other times it will be the resulting page of a particular action. This allows you to handle the WebDriver API inside the Page classes and removes the need of handling drivers in your actual test code.

</p>

Sample Login Page in Python

</p>

class LoginPage(object):</p>

def__init__(self, driver=None):
self.url = ''
self.driver = driver or webdriver.Chrome()
self.driver.get(self.url)</p>

def type_username(self, username):
self.driver.find_element_by_id('txtUserName').send_keys(username)
return self

def type_password(self, password):
self.driver.find_element_by_id('txtPassword').send_keys(password)
return self

def submit_login(self):
self.driver.find_element_by_id('btnlogin').click()
return HomePage(self.driver)

def main():
loginpage = LoginPage()
loginpage.type_username('jeff')
loginpage.type_password('password')
newpage = loginpage.submit_login()
</code></pre>

This quick set of code kind of illustrates the structure of the code. From the client perspective, it masks all of the nasty element searching and driver passing and makes the actual test code (the main routine) pretty clean and legible. Since a lot of testers aren't full blooded programmers, the ease of reading the main routine is probably going to be very appreciated.

</p>

Composite Objects and Inheritance

</p>

In most situations, a page has several common components. For example, on this blog, every page will have a navigation bar at the top, with menu items. The way I handled this scenario is by making the Navigation bar a composite object of the page. So the navigation is handled by this NavBarComponent. So in my example, I navigate menus by passing a list object that has the name of the navigation tree. So if the menu navigation would be Server->Inventory->Details, I'd pass a list of ['Server', 'Inventory', 'Details'].

</p>
class NavBarComponent:</p>

def navigate_menu(self, driver, *args):
for arg in enumerate(args):
hover = None
element = driver.find_element_by_partial_link_text(arg[1])
hover = ActionChains(driver).move_to_element(element)
hover.perform()
click = ActionChains(driver).click(element)
click.perform()
page_class = self.get_final_page_object(arg[1])
return page_class(driver)
</code></pre>

The get_final_page_object method is just a method that I created so that I can dynamically return a Page Object. That's not important however for our discussion here. So now, navigation can be handled by this subclass.

</p>
class HomePage(object):</p>

def __init__(self, driver):
self.navbar = NavBarComponent()
self.driver = driver

def navigate_menu(self, *args):
return self.navbar.navigate_menu(self.driver, *args)
</code></pre>

Now we have our HomePage object expose the navigate_menu method but simply passes that call to its composite NavBarComponent object. This allowed me to keep my code pretty focused on specific tasks. It also has the added benefit of being able to either update the NavBarComponent independently in the event it's ever changed or we can substitute the NavBarComponent for some specialty Navigation Bar in the event a page behaves differently than others.

</p>

I extended this same approach to things like DataGrids. Typically a DataGrid object will be reused from page to page. Sure simple things like column names may change, but just by parameterizing those small changes, you can easily use the Datagrid Object across multiple pages.

</p>

Inheritance

</p>

Lastly, we use inheritance to eliminate more code. In my project, we had a Server Inventory page, a Storage Inventory Page and a Network Device Inventory page. But all these pages had most of the same components and logic. That allowed me to place all the common actions into an InventoryPage class, which handled the nitty gritty things like, sorting by column name or grouping by column name. These are functional steps that would exist on each page. So by encapsulating all of those items into the InventoryPage base class, I can quickly add functionality to a bunch of different pages at once.

</p>

Here's a quick UML diagram of what this might look like.

</p>

UML Diagram of InventoryPages

</p>

This is by no means the absolute best way to do things. I'm sure there are plenty with more experience in Selenium that have other approaches. But this approach has been easy to maintain, easy to follow and easy to understand. I think that's what most of us are looking for in our Test Suites. Your mileage may vary.

</p>

comments powered by Disqus