Page Object Model (Selenium, Python)

We have come a long way since our post on implementing the Page Object Model
Implementing the Page Object Model (Selenium + Python)
While the above post is useful and we rank high on Google, we routinely hear two criticisms of it:
a) the post is too high level
b) the application being tested is not very repeatable

So we thought we would rework the above piece to address the drawbacks. In this post we shall give you a more detailed architecture, provide many more code snippets and write an automated test for Gmail.


Overview of Page Object Model

A page object represents an area in the web application user interface that your test is interacting with. Page objects reduces the amount of duplicated code and if the user interface changes, the fix needs changes in one place only.[1]

WHAT vs HOW
Usually the testers write the test cases describing ‘what’ is to be tested, this depends on the product functionality. But the implementation of this functionality by the developers keeps changing till the final code freeze is done, hence testers should know ‘how’ to implement the test cases so that the changes to the test scripts are minimal in case of code changes by the the developers. Page Objects encapsulates the finer details(locators and methods) of the pages from the test script and make the test script more readable and robust.


Sample Test Case – (WHAT)

We are going to explain about page objects with a very simple test case for Gmail.
-Goto http://gmail.com
-Enter the username, click Next
-Enter the password, click Sign in
-Perform search on the inbox ‘subject:POM’
-Click on the search result
-Click on inbox

A simple approach would be to write a test script with all the xpaths and the methods required for the execution of the above listed steps in one single file. The test would run fine and achieve the purpose but one major drawback is the test script is brittle. For any minor UI change on any page, the test script would have to be updated.To overcome this problem we use the page object pattern. As its name suggests,each page of the application to be tested is treated like an object which has the variables (xpaths) and methods (actions that can be performed on that particular page). This in turn makes the test script much cleaner.


Implementing the test case using POM templates (HOW)

Given below is the pictorial description of the various page objects used for the implementation of the test case.

page_object_model_classes


Lets start with the main hero – Page.py
All page models can inherit from the Page class. This has useful wrappers for common Selenium operations

class Page(unittest.TestCase):
    "Page class that all page models can inherit from"
 
    def __init__(self,selenium_driver,base_url='https://mail.google.com/'):
        "Constructor"
        #We assume relative URLs start without a / in the beginning
        if base_url[-1] != '/': 
            base_url += '/' 
        self.base_url = base_url
        self.driver = selenium_driver
        #Visit and initialize xpaths for the appropriate page
        self.start() 
        #Initialize the logger object
        self.log_obj = Base_Logging(level=logging.DEBUG)
 
 
    def open(self,url):
        "Visit the page base_url + url"
        url = self.base_url + url
        self.driver.get(url)
 
    def get_xpath(self,xpath):
        "Return the DOM element of the xpath OR the 'None' object if the element is not found"
 
    def click_element(self,xpath):
        "Click the button supplied"
    .
    .
    def write(self,msg,level='info'):
        self.log_obj.write(msg,level)
 
 
    def wait(self,wait_seconds=5):
        " Performs wait for time provided"
        time.sleep(wait_seconds)

Next is the Login_Page.py which handles the common functionality of user login. This will be the most re-used class.

from Page import Page
class Login_Page(Page):
    "Page object for the Login page"
 
    def start(self):
        self.url = ""
        self.open(self.url) 
        # Assert Title of the Login Page and Login
        self.assertIn("Gmail", self.driver.title)      
 
        "Xpath of all the field"
        #Login 
        self.login_email = "//input[@name='Email']"
        self.login_next_button = "//input[@id='next']"
        self.login_password = "//input[@placeholder='Password']"
        self.login_signin_button = "//input[@id='signIn']"
 
 
    def login(self,username,password):
        "Login using credentials provided" 
        self.set_login_email(username)
        self.submit_next()
        self.set_login_password(password)
        self.submit_login()
        if 'Qxf2 Mail' in self.driver.title :
            self.write("Login Success")
            return True
        else:
            self.write("FAIL: Login error")
            return False
 
    def set_login_email(self,username):
        "Set the username on the login screen"
 
    def submit_next(self):
        self.click_element(self.login_next_button)
        self.wait(3)
 
    def set_login_password(self,password):
        "Set the password on the login screen"
 
    def submit_login(self):
        "Submit the login form"

Once we login, the main page is displayed which consists of the header (which contains the search box, user profile options),the navigation menu on the left side of the page and the main body. As the header and the navigation menu are common to all pages we created page objects for each of them. Here is a snippet of each of the classes.

Nav_Menu.py

from Page import Page
class Nav_Menu(Page):
    "Page object for the side menu"
 
    def start(self):      
        "Xpath of all the field"
        #Navigation Menu
        self.inbox = "//a[contains(@href, '#inbox')]"
        self.sent_mail = "//a[contains(@href, '#sent')]"
        self.drafts= "//a[contains(@href, '#drafts')]"
 
 
    def select_menu_item(self,menu_item):
	"select menu item"

Header_Section.py

from Page import Page
class Header_Section(Page):
    "Page object for the page header"
 
    def start(self):
        "Xpath of all the fields"
        #Search and profile
        self.search_textbox = "//input[@id='gbqfq']"
        self.search_button = "//button[@id='gbqfb']"
        self.signout_button = "//a[text()='Sign out']"
        self.search_result = "//span[contains(text(),'%s')]"
 
    def search_by_subject(self,searchtext):
        self.set_text(self.search_textbox,'subject:'+searchtext)
      .
      .

Now, the Main_Page.py will contain the objects of the above two classes.

class Main_Page(Page):
    "Page object for the Main page"
 
    def start(self):
        self.url = ""
        self.open(self.url)
        #Create a Header Section object
        self.header_obj = Header_Section(self.driver)
        #Create a Menu object
        self.menu_obj = Nav_Menu(self.driver)

This completes the page objects needed for this particular test case.
**Please note – as an alternate way, we can also have a ‘Template_Page'(which inherits from the Page class and has the common objects) and have all pages(except Login page) derive from it.

In addition to these we have the following files
PageFactory.py
PageFactory uses the factory design pattern. get_page_object() returns the appropriate page object.

def get_page_object(page_name,driver,base_url='https://gmail.com/'):
    "Return the appropriate page object based on page_name"
    test_obj = None
    page_name = page_name.lower()
    if page_name == "login":
        test_obj = Login_Page(driver,base_url=base_url)
    elif page_name == "main":
        test_obj = Main_Page(driver,base_url=base_url)  
 
    return test_obj

DriverFactory.py which returns the appropriate driver for firefox or chrome or IE browser.
login.credentials file contains the username , password required for authentication.

Finally , we have our test script which puts it all together and executes the test case.
Search_Inbox_Test.py

def run_search_inbox_test(browser,conf,base_url,sauce_flag,browser_version,platform,testrail_run_id):
    "Login to Gmail using the page object model"
    # get the test account credentials from the .credentials file
    credentials_file = os.path.join(os.path.dirname(__file__),'login.credentials')
    username = Conf_Reader.get_value(credentials_file,'LOGIN_USER')
    password = Conf_Reader.get_value(credentials_file,'LOGIN_PASSWORD')
 
    #Result flag used by TestRail
    result_flag = False
 
    #Setup a driver
    #create object of driver factory
    driver_obj = DriverFactory()
    driver = driver_obj.get_web_driver(browser,sauce_flag,browser_version,platform)
    driver.maximize_window()
 
 
    #Create a login page object
    login_obj = PageFactory.get_page_object("login",driver)
    if (login_obj.login(username,password)):
        msg = "Login was successful"
        result_flag = True
        login_obj.write(msg)
    else:
        msg = "Login failed"
        login_obj.write(msg)
 
    #Create an object for main page with header and menu
    main_obj = PageFactory.get_page_object("main",driver)
    main_obj.wait(3)
 
    #Search the inbox for message by subject 'POM' and open the message
    if main_obj.header_obj.search_by_subject('POM'):
        main_obj.write("Search successful")
        result_flag = True
    else:
        main_obj.write("Search text was not found")
        result_flag = False
 
    #Go to inbox
    main_obj.menu_obj.select_menu_item('inbox')

As you must have noticed the final test is very easy to read and need not be modified in case of any underlying changes to individual pages.

Running the test

Let us execute the test,
run_test_page_object_model

and here is the log file for the test run.
log_file_page_object_model


So now, we all agree that the page objects make it really easy for the tester to convert the documented test case to an automated test case. Not only that, maintaining these tests is also very easy.

Don’t want to implement a Pythonic test automation framework from scratch? You should try our open-sourced Web automation framework based on the page object model.

If you are a startup finding it hard to hire technical QA engineers, learn more about Qxf2 Services.


References:

1. http://selenium-python.readthedocs.org/en/latest/page-objects.html
2. https://code.google.com/p/selenium/wiki/PageObjects
3. Batman and Page Objects


Subscribe to our weekly Newsletter


View a sample



29 thoughts on “Page Object Model (Selenium, Python)

    1. I am a beginner on Python, I had been using Java/Selenium on my previous projects.
      This is a great help for people like me to set up a framework in Python.
      Could you please explain the functionality and purpose of TestRail

      1. Hi Shiva,
        Iam a manual tester and i like to shift to python automation. Can you please help me where to start .Iam a beginner for automation. if anyone can help me please reach me out through my mail : [email protected], phone: 8122651213

      2. Hi Suresh,
        Nice to see you want to look at python automation. Ideally, I would suggest you start practicing automation regularly and improve over time. Try to produce working code always. Don’t look at it from an angle of learning a language and then learning selenium or any other tools.
        You can start here Selenium tutorial for beginners, then move on to automating something a bit more complex eg: http://weathershopper.pythonanywhere.com/.
        You can find a lot of help and information from our friend Google, but the key again is doing it regularly. Hope this helps you get started.

  1. Just read through the tut. Is it incomplete?

    I don’t think the code examples can be entered to get the test results as indicated @ the end of the article. (the last article was able to be duplicated in its entirety, which was great!)

      1. know it’s late, but thanks!

        your tutorials have been incredibly helpful in my company setting up similar tests.

        what are you guys using to manage multiple browser types?

      2. Excellent! Adrian, we are glad to hear we are helping you and your company test better.

        We typically run our cross browser tests on BrowserStack or Sauce Labs. We do not maintain local test machines with different browsers – because its a pain to do so and not as cost-effective as using a cloud service provider.

        While developing test scripts, we make sure our test scripts accepts the Browser and version as command line parameters. We then have one script that loops through a set of defined browsers and runs each test. You can use Python’s subprocess module to call the test itself. The one wrinkle that we have not yet been able to iron out is how we report the test runs for each browser to our test case management system – TestRail. But other than that, things look good.

        We are fully booked till the early December. The posts you see us publish have already been written and scheduled for publication. But as soon as we free up, we will write about running tests across multiple browser types. In the meantime, let me know if you get stuck in any specific step when implementing this approach.

  2. Hey Guys,i am new to python with selenium.i have gone through your post it is really helpful .

    Just want to know whether you guys are providing any online training for Selenium with Python

    1. Hey, D, this is definitely not Java-ish. Not even close. We have implemented the Page Object pattern in both Java and Python and I can assure you that they look very, very different. I like our Python implementation so much more because its … well, Pythonic. But, like you mentioned, both work.

      There are language differences between Java and Python that make it very hard to use the same patterns. E.g.: when it comes to Page Objects, Java provides a ‘synchronized’ keyword that makes multiple classes inheriting from the same Base page instance very easy. We had to solve that differently in Python with a Borg pattern – approximately 10 more lines of code.

      This isn’t the full implementation – just a guide to get you started. Some day we will publish our entire framework (its a 9+ post series) and hopefully you’ll notice how Pythonic it is compared to the link you provided.

  3. Terrific articles folks. Really appreciate this level of effort and its very helpful

    Will be nice if you could share git repo for other tremendous articles.

    1. Hi Ganesh, Thank you for your comment. We are working on it as part of other articles.

  4. hi,
    I am new to python and also for selenium web driver.
    Above the example is a little bit confusing me, if possible can you please provide another simple example for page object model plz…

  5. @Arunkumar Muralidharan: Thanks for your reply.
    Can you just provide me a login example for “HOW TO READ AND FETCH THE VALUES FOR CONFIG PROPERTY FILE?

    1. @G if you are starting off, simply hard code the values.

      If you are a bit more comfortable with Python, try a simple example of login like so:

      1. Look at the Conf_Reader.py here: https://github.com/qxf2/GMail/blob/master/Conf_Reader.py
      2. Create a config file like here: https://github.com/qxf2/GMail/blob/master/login.credentials
      3. Then, in your test, read values like so:

      import Conf_Reader
       
      conf_file = "YOUR CONFIG FILE"
      username = Conf_Reader.get_value(credentials_file,'LOGIN_USER')
      password = Conf_Reader.get_value(credentials_file,'LOGIN_PASSWORD')

      You can look at lines 35-37 over here for an example: https://github.com/qxf2/GMail/blob/master/Search_Inbox_Test.py

      NOTE: We use a more advanced version of this technique in our framework. It would take me quite a lot of time to outline it fully here. But given that you are starting off, the above code is good and easily extensible.

  6. Hello Everyone,
    Thanks for this Article. I am trying to implement page object pattern in my automation framework. I liked your solution very much. I wanted some help on below mentioned topics. Please share your thoughts , it would be very helpful for me to take the design decision.
    1- I was looking at other Python based page object models. Most of them have used __get__ , __set__ properties of attributes. In your implementation that is not the case. Kindly tell me if it is advantageous to use properties in page objects.
    2- You have invoked unittest in base class, then inherited other classes from it. There is another approach I saw where they put unittest module in test script only and do all the assertions there. Which path would be better take.

    1. Abhishek,

      1. Either approach works. The __get__ and __set__ pattern is probably the right way to implement a “true” object-oriented approach. I don’t find using __get__ and __set__ very Pythonic. So we skipped doing that in favor of keeping the code simple.

      2. Again, either approach works :). We compose our Base class with unittest so that if we chose to have assertions at the page object level instead of the test level, things would work just fine too.

      BTW, we have open sourced our framework. We have not yet gotten around to documenting it thoroughly, but if you are comfortable with Python, take a look at the tests and work your way from there.

      Open sourced Python page object model: https://github.com/qxf2/qxf2-page-object-model

      1. Thanks for the explanation and link of the code base :). I will go through it and try understand how things are fitting together.

  7. Really very helpful. We are using pytest as our framework. This tutorial helped to develop reasonable framework at my work. Can we except framework using pytest. Also i have learn whole thing which is mentioned in your article but i’m not clear how to maintain test data. As i know in selenium java, we use exel sheet and using AutoIt we can get the test data.
    But here you are using the below code:

    # get the test account credentials from the .credentials file
    credentials_file = os.path.join(os.path.dirname(__file__),’login.credentials’)
    username = Conf_Reader.get_value(credentials_file,’LOGIN_USER’)
    password = Conf_Reader.get_value(credentials_file,’LOGIN_PASSWORD’)

    Please explain about this…

    1. Hi Mubarak,
      We used to maintain our data in a conf file and then using a Conf Reader file loaded the data in our test.
      However, we no longer use conf file and we store data in testdata_set.py file. We then import the .py file into our test.
      In case you need the way we do it you can have a look at our framework here. We have open sourced our framework.

      Thanks & Regards
      Avinash Shetty

  8. Hi Avinash,
    Thanks for the nice article. Fro your above comments, i understand you have changed the way test data is stored. But for the gmail example, i suppose we still have to update credentials in the login.credentials file.
    I gave the credentials as below:
    LOGIN_USER=pradeepbhat
    LOGIN_PASSWORD=abcd
    But was getting the error -“Unable to locate the provided config file: E:\pradeep\python projects\GMail-master\data.conf”.
    Hence i tried creating the data.conf by giving in the same format, but got the below error:
    Exception in get_value
    file: login.credentials
    key: LOGIN_USER
    Exception in get_value
    file: login.credentials
    key: LOGIN_PASSWORD
    Please help in understanding how to input the test data.

    1. Hi Pradeep,
      “Unable to locate the provided config file: E:\pradeep\python projects\GMail-master\data.conf” – Did you have the Search_Inbox_Test.py file in the GMail-master directory?

      1. Yes Hari. I’ve not made any changes to the directory. In fact when i tried giving the user name and password directly, instead of reading through the .credentials files, it worked fine. It seems it is unable to read the contents in the “login.credentials” file.

        def run_search_inbox_test(browser,conf,base_url,sauce_flag,browser_version,platform,testrail_run_id):
        “Login to Gmail using the page object model”
        # get the test account credentials from the .credentials file
        credentials_file = os.path.join(os.path.dirname(__file__),’login.py’)
        print(credentials_file)
        username = Conf_Reader.get_value(credentials_file,’LOGIN_USER’)
        password = Conf_Reader.get_value(credentials_file,’LOGIN_PASSWORD’)
        #Bypassing reading from the credentials file
        username=”pradeep”
        password=”abcd”
        #Result flag used by TestRail
        result_flag = False

      2. Pradeep,

        # get the test account credentials from the .credentials file
        credentials_file = os.path.join(os.path.dirname(__file__),’login.py)

        Is your credentials in login.credentials or is it in login.py file?
        Using .py files reduces the number of lines of code needed to read from a file. Assuming you have your test file in GMail-master directory and login.py in a conf directory, i.e conf is a child dir in GMail-master.
        This is how your Search_Inbox_Test.py should look,

        import os,sys
        sys.path.append(os.path.dirname(os.path.abspath(__file__)))  #Adding the GMail-master dir to system path so that login.py can be imported from conf
        print os.path.dirname(os.path.abspath(__file__))             #This helps in debugging.
        from conf import login                                       #Import the login file in the conf dir
        def run_search_inbox_test(browser,conf,base_url,sauce_flag,browser_version,platform,testrail_run_id):
            "Login to Gmail using the page object model"
            # get the test account credentials from the .credentials file
            username = login.name
            password = login.pswd

        And this is how your login.py should look,

        name = "pradeep"
        password = "abcd"

        Pls let me know if this helped.

Leave a Reply

Your email address will not be published. Required fields are marked *