Monday, December 31, 2012

Python : Quick and Dirty PageObject Pattern

One of the basic of good test automation design is to keep low level actions separate from high level business rules.  One way of doing this is keeping a consistent theme of separating screens, windows, and dialogs into manageable self contained objects that hide all the low level UI interactions and only expose higher level transactional actions.

While Java has an official PageFactory implementation.  Those of us using Python are left trying to figure out what works best out of the the many implementations out there.   Here are a few implementations I find notable.
However,  I wanted something slightly different.  1st example I thought was a bit annoying to have to create wrappers for all the page elements, while the 2nd one was trying to implement a test framework (something which I already have).  The 3rd I felt was not a very good way of implementing Page Objects due to having hard coded locators in the method bodies and not enforcing page validation.

So I started thinking of how I'd implement one that was very simple, light weight and easy to understand.  I want to make one that was easy to maintain, debug, and have a very elegant syntax during usage.

Creating a quick and dirty Page Object Base Class

At the very essence, I need the Page Object to do the following:
  1. Validate we are on the correct page.
  2. Keep track of it's elements in 1 centralized place in the Page Object class (all in the top section)
  3. Hide UI level logic and only expose transactional level interactions.

Handling Page Self Validation

To handle the aspect of making pages self validating, we can accomplish this using a simple base class with an abstract method for validating the page.  Then in our PageObject constructor we can make a call to validate the page when the constructor/initializer is called.


import abc

class PageObject(object):

    # Webdriver associated with this instance of the PageObject
    webdriver = None

    def __init__(self, webdriver):
        self._validate_page(webdriver)
        
        self.webdriver = webdriver

    @abc.abstractmethod
    def _validate_page(self, webdriver):
        """
        Perform checks to validate this page is the correct target page.
        
        @raise IncorrectPageException: Raised when we try to assign the wrong page 
        to this page object.
        """
        return

class InvalidPageError(Exception):
    '''Thrown when we have tried to instantiate the incorrect page to a PageObject.'''

Now classes that inherit from PageObject will enforce PageValidation.  In this base PageObject class, we also assign the WebDriver instance, which will be useful for the next step below.

Handling Page Element Mapping


To keep all the Page Element Mappings in one place, I want to accomplish this by making all page element mappings a Class property that's evaluated during run time.  There's 2 ways we can effectively do this in Python, 1) Using class decorators to simplify writing getter methods, or 2) Using Python's lambda expressions to create easy 1 line anonymous functions that are called at run time.  I felt the 2nd was a lot less work to do, so I opted for the latter.  As you can see below, using the lambda functions I'm able to effectively keep all my locators in 1 place in my PageObject class, where they're all in one place and easy to maintain.


class ProjectHomePage(PageObject):
    '''
    Page Object for Project Home Page (page when you view an individual project)
    '''

    # Identifying Properties #
    _BODY_TAG_ID = "project_home"

    ### Web Element Identifiers ###
    to = lambda self:self.webdriver.find_element_by_css_selector("#send-presentation-overlay .to-textarea")
    subject = lambda self:self.webdriver.find_element_by_xpath("//*[@id='send-presentation-overlay']//input[@name='subject']")
    message_text = lambda self:self.webdriver.find_element_by_css_selector("#send-presentation-overlay .message-textarea")
    send_button = lambda self:self.webdriver.find_element_by_css_selector("#send-presentation-overlay .submit")
...
    def send_message(self, to_address, subject, message):

        self.to().send_keys(to_address)
        self.subject().send_keys(subject)
        
        #switch message input to text input to make it easier to set the message.
        self.message_text().send_keys(message)

With the mapping handled by lambda expression, how you can use this lambda expression to referencing the mappings by calling it as a member method. These lambda expressions are handled during runtime and will return the webelement requested using the stored instance of WebDriver from the base PageObject.

Another added benefit of using lambda expressions is it works very well with WebDriverWait statements.  This is very useful for Ajax pages where you are frequently waiting for elements be become visible/enabled. (Note: this is a bit hackish in how I pass in 'self' instead of the webdriver in the webdriver wait. But the end result is the same, the lambda function has a direct reference to the webdriver through the '.webdriver' property.)


WebDriverWait(self, 10).until(self.send_button).click()

How it comes together to hide low level details

Once you have a bunch of these low level transactional details wrapped into PageObject calls.  Writing your high level tests should be a simple matter of just calling your PageObject's methods.  This will automatically do the page validation and perform the actions.


class TestProjectPageTests(unittest.TestCase):
    
    def test_send_message_to_project_manager(self):
        driver = firefox.webdriver.WebDriver()
        LoginPage.go_to_page(driver)
        LoginPage(driver).login("user","password")
        HomePage(driver).go_to_project_page()
        ProjectsListPage(driver).open_project("Project1")
        ProjectPage(driver).send_message_to_project_manager("test message")
        ...

There you have it.  A simple PageObject implementation that not that hard to implement, and does wonders for cleaning up your high level test syntax.



Update:

Since I've written this article a while back.  I've put a lot of this implementation into Web Test Framework (WTF), which my company has open sourced.  You can find WTF, here:  https://github.com/wiredrive/wtframework

2 comments:

Unknown said...

Hi David

I've stumbled upon your article and found it very helpful. I'm interested in idea to use lambda functions as a locators, but I'm stuck with WebDriverWait statements.

Another added benefit of using lambda expressions is it works very well with WebDriverWait statements. This is very useful for Ajax pages where you are frequently waiting for elements be become visible/enabled. (Note: this is a bit hackish in how I pass in 'self' instead of the webdriver in the webdriver wait. But the end result is the same, the lambda function has a direct reference to the webdriver through the '.webdriver' property.)

I'm trying to repeat your trick by passing 'self' instead of the webdriver in the webdriver wait, but fail with the following exception:

() takes 1 positional argument but 2 were given

class WebDriverWait(object):

def __init__(self, driver, ...):
...
self._driver = driver


def until(self, method, message=''):
...
while(True):
try:
value = method(self._driver)
...

It seems that in this case 'self' reference is sent twice, how did you manage to get it working?


P.S.: I'm trying to make the simplest solution possible, so I want to utilize the basics described in your article. WTF framework is awesome, but is not an option for me.

Please advise,
Yuriy

Jeremy / 傑洛米 said...

Inspired by this great article, I have created a module called pageobject_support that implements PageFactory pattern in a pythonic way. Your feedback is appreciated.