Using the Page Object test design pattern

Last edited on

Table of Content

Introduction

Page Objects are a widely used design pattern for test automation frameworks. It allows the abstraction of Squish APIs behind frames/windows that specify the user interaction in more intelligent and meaningful ways. With Page Objects, it is simple to encapsulate test script code and make future maintenance of test scripts easy and much less painful. Designing your test automation solution based on Page Object patterns requires the creation of two layers:

Splitting your test automation framework into layers has many advantages. One of them is a better allocation of team members working on test automation. Those with technical and programming knowledge develop and maintain the test automation framework. Using this custom framework, your QA Specialists, having a vast knowledge of the application functionality, but without programming skills will be able to create robust, maintainable and efficient automated test cases easily. Other advantages include reduced maintenance effort, more straightforward troubleshooting and faster adaptation for ever-changing applications under test.

Business layer

This layer contains test script code at a very abstract level, easy to create and read even by non-technical team members. Test cases are very short and self-explanatory, focused on functionality tested, not on how the application under test is built. While applications evolve, test script remains unchanged (unless application functionality changes).

Technical layer

This layer contains interaction with the Application Under Test using the Squish API. It takes full advantage of Object Oriented Programming, enabling modular design and a reusable test automation framework. In general, a Page Object is a class representing a part of an application (usually a single page, frame, sub-frame, window or dialogue). This class provides an interface to interact with the application at very abstract level. Page Object frameworks must be designed to fulfill the following rules:

Example

Let’s start by defining what services the AddressBook frame offers a user. In this frame we can create a new entry, save or edit an address book. Additionally the user can view all entries in the address book. In the example below the AddressBook class is created offering the following methods: new (creating new address book), add (adding entry to existing address book), entries (returning lists of all entries in address book), rowcount (returning number of entries in address book).

class AddressBook(PageObject):
    FRAME = ":Address Book_AddressBook"
    TABLE = ":Address Book*_JTable"
    MENU = ":Address Book_JMenuBar"

    FILE_MENU = ":File_JMenu"
    EDIT_MENU = ":Edit_JMenu"

    def __init__(self):
        test.log("Opening Address Book")
        waitForObject(self.FRAME)

    def new(self):
        test.log("New Address Book")
        activateItem(waitForObjectItem(self.MENU, "File"))
        activateItem(waitForObjectItem(self.FILE_MENU, "New..."))

    def add(self):
        test.log("Add entry to Address Book")
        activateItem(waitForObjectItem(self.MENU, "Edit"))
        activateItem(waitForObjectItem(self.EDIT_MENU, "Add..."))
        return AddressBookAdd()

    def entries(self):
        ''' Returns list of lists with Address entries '''
        jtable = waitForObject(self.TABLE)
        ee = []
        for row in range(jtable.getRowCount()):
            rr = []
            for col in range(jtable.getColumnCount()):
                rr.append(str(jtable.getValueAt(row, col)))
            ee.append(rr)
        return ee

    def rowscount(self):
        ''' Returns the number of rows in Address Table'''
        return waitForObject(self.TABLE).getRowCount()
AddressBook.py

In the AddressBook class method add returns a new Page Object, because after pressing the menu option “Add”, the new dialog window displays as “AddressBook - Add”. Therefore this window is represented by a separate Page Object called AddressBookAdd.

class AddressBookAdd(PageObject):
    DIALOG_ADD = ":Address Book - Add_Dialog"
    TEXTFIELD_FORNAME = ":Address Book - Add.Forename:_JTextField"
    TEXTFIELD_SURNAME = ":Address Book - Add.Surname:_JTextField"
    TEXTFIELD_EMAIL = ":Address Book - Add.Email:_JTextField"
    TEXTFIELD_PHONE = ":Address Book - Add.Phone:_JTextField"
    BUTTON_OK = ":Address Book - Add.OK_JButton"
    BUTTON_CANCEL = ":Address Book - Add.Cancel_JButton"

    def __init__(self):
        test.log("Opening Address Book - Add window")
        waitForObject(self.DIALOG_ADD)

    def ok(self):
        test.log("Press OK button to add new entry to address book")
        clickButton(waitForObject(self.BUTTON_OK))

    def cancel(self):
        test.log("Press Cancel button")
        clickButton(waitForObject(self.BUTTON_CANCEL))
AddressBookAdd.py

In above examples, we use class constructor (init method) as a place for extra synchronization and logging. Any extra code that needs to be executed when a new frame is opened can be put there.

The last piece of the framework is the PageObject class, which contains generic methods, which can be used for every frame. Every PageObject defined in test framework inherits from the PageObject class. In this example, we defined method fill to enter text into any text field in the frame.

class PageObject:
    def fill(self, **kwargs):
        for key, value in kwargs.iteritems():
            type(waitForObject(getattr(self, key)), value)
PageObject.py

Finally, we need to prepare the test case that will check a basic scenario in the address book application.

source(findFile("scripts", "PageObject.py"))
source(findFile("scripts", "AddressBook.py"))
source(findFile("scripts", "AddressBookAdd.py"))

def main():
    startApplication("AddressBookSwing.jar")
    addressBook = AddressBook()
    test.compare(addressBook.rowscount(), 0, "AddressBook.rowscount?")
    addressBook.new()
    addressBookAdd = addressBook.add()
    addressBookAdd.fill(TEXTFIELD_FORNAME="Tom", TEXTFIELD_SURNAME="Paw", TEXTFIELD_EMAIL="tp@m.com", TEXTFIELD_PHONE="1234")
    addressBookAdd.ok()
    test.compare(addressBook.rowscount(), 1, "AddressBook.rowscount?")
    test.compare(addressBook.entries()[0], [], "AddressBook.entries?")
test.py

After executing the test case in the Squish IDE, we can check the Test Results view. All test steps are generated there by the Page Object framework, and therefore are consistent with all test cases for the given application. Moreover, each log represents functional steps in the test cases, which makes an analysis of potential defects in the application more convenient.

Squish IDE: Test Results View

Additional reading and materials

Article about PageObject by Martin Fowler

suite_PageObjects_py.zip