(POM) Page Object Model Structure?

Can anyone please share the snapshot of - how the project structure looks like while using Page Object Model in Katalon for API Automation project.

Is this what you want to see?

image

I understand the term “Page Object Model” as described in Page Object Model (POM) & Page Factory in Selenium

I suppose nobody has ever created such project.

I am afraid, you are mislead by the document Katalon Docs/ Page Object Model.

This document is NOT explaining how you can apply “Page Object Model” in a Katalon Studio project. This document is discussing about “Katalium Framework”. “Katalium Frame” is a Groovy Library. It lets you get out of Katalon Studio GUI, go back to a plain Java/Groovy project with the Selenium Webdriver API plus “Katalium Framework” which lets you write your Selenium-based WebUI tests in Katalon-fravoured style.

I believe that the Katalon team has never officially stated that you can apply the Page Object Model into a Katalon Studio project.

Well, answer is that it will be a usual Java project, which could have many many variations The project structure depends which build system you use — Gradle, Eclipse, Ant. … I am afraid, this is not the answer you expected.

The term “Page Object Model” is applied only to Web UI tests. “Web UI Automation” means that your tests will target HTMLs. So, you will primarily use org.openqa.selenium.By class to look into the target HTML for data to verify. The term “Page Object Model” isn’t relevant to RESTful/SOAP API testing.

“API Automation” means that your tests will target JSON. Then you may want to use JMESPath to traverse the JSON content. If you are targetting SOAP XML, then javax.xml.xpath.XPath would be perfect.

You would use different helper classes but the way you perform verifications are quite similar. So, I can imaging a term “REST-Response Object Model” for REST-API testing, in synonym to “Page Object Model” for WebUI test.

But I have never seen someone has ever used the term “REST-Response Object Model”. I guess, this term is not necessary. Every single JSON document will be converted into a Java/Groovy object once, then you get access to it. All of them are “Object”. No need to invent a special term.

On the other hand, an HTML document is too complex to regard an “Object” with easy accessors. We need to write custom getter methods for data in a HTML document. So a special term “Page Object Model” was invented and accepted.

I have ever developed a Katalon Studio project for WebUI Automation (not REST API Automation) applying Page Object Model.

Here is a Test case that uses my “Page Objects”:

One of my “page object” is here for example

This “Page Object” has a line

	static final By APP_HEADER      = By.xpath('//h1[contains(text(),"Flaskr")]')

Here I use org.openqa.selenium.By class to make a query for a tex “Flaskr” in the target HTML.

You can imagine, you want to use JMESPath instead of By. Then your “REST-Response Object Model” would be ready to verify JSON input.

By the way,
You would find that the structure of this project is unusual as a Katalon Studio project. The Test Case flaskr/TC3_Alice_and_Bob_interact is short; it uses no Test Object. It uses very little of WebUI.* keywords. There are many codes in the Include/scripts/groovy folder that implement methods to read data out of HTML and perform verifications. This project has such unusual structure because the “Page Object Model” does not fit in a Katalon Studio project very well.

The POM is really a design pattern for Object-oriented programming (OOP). Katalon Studio is not designed with OOP in mind. Katalon Studio is good for non-programmers. It’s main features (Test Objects, Test Case, Built-in Keywords) are designed to be friendly for the programming-beginners. But these features rather disturb the programming-experts who want to use OOP design patterns including POM.

@manik.tandon

You wrote you want to apply POM to your project. Then you must be a real programer. You would find it easier to apply POM in a usual Java/Groovy project with Selenium WebDriver & Apache HTTP Client on Gradle/Maven/Ant, than in Katalon Studio.

Finally, let me explain what is the very reason why I think Katalon Studio and POM does not fit. Katalon Studio does not help you to do unit-testing for the classes you wrote. If you are to use Page Object Model, then you will write many "class"es in Groovy or Java. If you are to write “classes”, you would need to write and execute JUnit tests intensively. Katalon Studio does not help you to do JUnit-based tests at all. Katalon Studio intentionally hides Eclipse’ JUnit support (once I tried to restore it by my custom keyword ). Why they did so? I guess that Katalon thought that it is better to hide the name “JUnit” from the users perspective in KS for simplicity. And they thought that all codes you write (or generated by the Recorder tool) in a KS project with Built-in Keywords & Test Objects will be easy to debug; you would not want unit-tests on your codes. This assumption is valid if you are a programming beginner who don’t know what the “class” keyword is. But a seasoned OOP programmer will find that it is difficult to develop a fair scale of class library in Katalon Studio. In short, you are not supposed to do any OOP in Katalon Studio.

In fact, I developed my “POM” class library for the sample project above in another Gradle-backed project. There I did enough JUnit-based testing. And once completed, I copied the Java source code of my POM into a Katalon Studio project to reuse it.

Katalon Studio has its own test object repository, in which go the test objects on a page. I guess this is their answer to the “page object model”, which can make your page classes quite…busy.

But, there are still many reasons for explicitly implementing the page object model in Katalon Studio (and why I refactored my project to using it), including:

  • a lot of the same steps, for just one action
  • same action shared across families of test cases
  • to encapsulate/abstract out a lot of busy/technical work, including
  • programmatic test objects/web element collections
  • shared functionality between pages, including how to scroll down

That all sounds great! but how the hell do I take advantage of all that?

Like this:

  • you don’t have to use WebElements in your page classes. You may use TestObjects.
  • same core principles apply: separation of UI interaction concerns from those of validation.

Basically, just write the class how you would if it were Selenium, but remember that the “web element” you’re looking for, is probably somewhere in the Object Repository for the page that you’re writing the class for. This will save you from a lot of field-bloat.

Real example of page object from my code base

I have test cases that cover two very similar pages on my AUT: a member list page, and a member lead list page. Also, I have test cases against member leads and members respectively.

This is perfect time to use page objects!

I have some BasePersonListPage, defined to be:

package com.signaturemd.pages.personList

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import org.openqa.selenium.Keys

import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.signaturemd.constants.personListPage.PersonListPageFilterNames
import com.signaturemd.models.person.BasePersonModel
import com.signaturemd.pages.BaseCRMPage
import com.signaturemd.profiles.PracticeProfile

import groovy.transform.InheritConstructors

@InheritConstructors
public abstract class BasePersonListPage extends BaseCRMPage {
	public static final String FILTER_SIDEBAR = 'Page_Member List/Filter Sidebar/Filter By Fields Subsection/'

	public void initFilters(PracticeProfile profile) {
		this.searchAndSetFilter(PersonListPageFilterNames.ORGANIZATION,
				this.getOrganizationFilterCheckbox(),
				this.getOrganizationFilterTextField(),
				profile.getOrganizationName())

		this.searchAndSetFilter(PersonListPageFilterNames.PRACTICE_NAME,
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Practice Name Filter/Practice Name checkbox'),
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Practice Name Filter/Practice Name text field'),
				profile.getPracticeName())
	}

	public void applyFamilyDiscountFilter() {
		this.searchAndSetFilter(PersonListPageFilterNames.FAMILY_DISCOUNT,
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Family Discount Filter (A)/Family Discount checkbox'),
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Family Discount Filter (A)/Family Discount TF'),
				"0")

		WebUI.click(findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Family Discount Filter (A)/Relation dropdownBtn'))

		final TestObject greaterThanDropdownOption = findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Family Discount Filter (A)/Greater Than option');

		WebUI.waitForElementVisible(greaterThanDropdownOption,
				1)

		WebUI.click(greaterThanDropdownOption)
	}

	public void applyNameFilters(BasePersonModel model) {
		if ((model.getPrefix() != null) && (!model.getPrefix().equals(""))) {
			final TestObject namePrefixTestObject = findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Name Prefix Filter/Name Prefix text field')

			this.searchAndSetFilter(PersonListPageFilterNames.PREFIX,
					findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Name Prefix Filter/Name Prefix checkbox'),
					namePrefixTestObject,
					model.getPrefix())

			WebUI.click(namePrefixTestObject)

			TestObject prefixDropdownOption = this.getPrefixDropdownOption(model.getPrefix())

			WebUI.waitForElementVisible(prefixDropdownOption, 2)

			WebUI.click(prefixDropdownOption)

			WebUI.sendKeys(namePrefixTestObject,
					Keys.TAB.toString())
		}

		this.searchAndSetFilter(PersonListPageFilterNames.FIRST_NAME,
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/First Name Filter/First Name checkbox'),
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/First Name Filter/First Name text field'),
				model.getFirstName())

		this.searchAndSetFilter(PersonListPageFilterNames.LAST_NAME,
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Last Name Filter/Last Name checkbox'),
				findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Last Name Filter/Last Name text field'),
				model.getLastName())

		if ((model.getSuffix() != null) && (!model.getSuffix().equals(""))) {
			final TestObject nameSuffixTextField = findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Name Suffix Filter/Name Suffix text field')

			this.searchAndSetFilter(PersonListPageFilterNames.SUFFIX,
					findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Filter By Fields Subsection/Name Suffix Filter/Name Suffix checkbox'),
					nameSuffixTextField,
					model.getSuffix())

			WebUI.click(nameSuffixTextField)

			TestObject suffixDropdownOption = this.getSuffixDropdownOption(model.getSuffix())

			WebUI.waitForElementVisible(suffixDropdownOption, 2)

			WebUI.click(suffixDropdownOption)

			WebUI.sendKeys(nameSuffixTextField,
					Keys.TAB.toString())
		}
	}

	public void searchAndSetFilter(String filterName, TestObject filterCheckbox, TestObject filterTextField, String input) {
		WebUI.setText(findTestObject('Page_Zoho shared repository/CRM pages/Person List pages/Search text field'), filterName)

		WebUI.scrollToElement(filterCheckbox, 2)

		WebUI.waitForElementVisible(filterCheckbox, 2)

		WebUI.click(filterCheckbox)

		WebUI.waitForElementPresent(filterTextField, 2)

		WebUI.setText(filterTextField, input)
	}

	public TestObject getPrefixDropdownOption(String prefix) {
		return new TestObject("${this.FILTER_SIDEBAR}/Name Prefix Filter/${prefix} dropdown option")
				.addProperty("xpath",
				ConditionType.EQUALS,
				"//*[@data-zcqa='Name Prefix_${prefix}']")
	}

	public TestObject getSuffixDropdownOption(String suffix) {
		return new TestObject("${this.FILTER_SIDEBAR}/Name Suffix Filter/${suffix} dropdown option")
				.addProperty("xpath",
				ConditionType.EQUALS,
				"//*[@data-zcqa='Suffix_${suffix}']")
	}

	public void waitForTableLoad() {
		WebUI.waitForElementNotPresent(this.getFirstResult(), 2)

		WebUI.waitForElementVisible(this.getFirstResult(), 2)
	}

	public abstract TestObject getFirstResult();
	protected abstract TestObject getOrganizationFilterCheckbox();
	protected abstract TestObject getOrganizationFilterTextField();
}

and the MemberListPage and MemberLeadListPage are defined to be:

package com.signaturemd.pages.personList

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import com.kms.katalon.core.testobject.TestObject

public class MemberListPage extends BasePersonListPage {
	public MemberListPage() {
		super("https://crm.zoho.com/crm/org000000000/tab/CustomModule4/custom-view/4623170000000547036/list?page=1");
	}

	@Override
	public TestObject getFirstResult() {
		return findTestObject('Page_Member List/Member Table/First Member Name link');
	}

	@Override
	protected TestObject getOrganizationFilterCheckbox() {
		return findTestObject('Page_Member List/Filter SIdebar/Filter By Fields Subsection/Organization Filter/Organization checkbox');
	}

	@Override
	protected TestObject getOrganizationFilterTextField() {
		return findTestObject('Page_Member List/Filter SIdebar/Filter By Fields Subsection/Organization Filter/Organization text field');
	}
}

package com.signaturemd.pages.personList

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import com.kms.katalon.core.testobject.TestObject

public class MemberLeadListPage extends BasePersonListPage {
	public MemberLeadListPage() {
		super("https://crm.zoho.com/crm/org000000000/tab/Potentials/custom-view/4623170000000087545/canvas/4623170000000293236");
	}

	@Override
	public TestObject getFirstResult() {
		return findTestObject("Page_Member Lead List/Member Table/First Member Lead Name link");
	}

	@Override
	protected TestObject getOrganizationFilterCheckbox() {
		return findTestObject('Page_Member Lead List/Filter Sidebar/Filter By Fields Subsection/Organization Filter/Organization checkbox')
	}

	@Override
	protected TestObject getOrganizationFilterTextField() {
		return findTestObject('Page_Member Lead List/Filter Sidebar/Filter By Fields Subsection/Organization Filter/Organization text field');
	}
}

That makes my member test case as simple as, for example:

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject

import java.util.concurrent.TimeUnit

import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.signaturemd.factories.MemberFactory
import com.signaturemd.factories.MemberLeadFactory
import com.signaturemd.models.member.BaseMemberModel
import com.signaturemd.pages.personList.MemberListPage
import com.signaturemd.profiles.PracticeProfile
import com.signaturemd.utils.ActionHandler
import com.signaturemd.utils.SMDNumberUtils
import com.signaturemd.utils.SMDStringUtils
import com.signaturemd.utils.SMDTestCaseUtils

'Type-casting for IntelliSense'
practiceProfile = (PracticeProfile)practiceProfile;

BaseMemberModel model = new MemberFactory().createMainMember();

TestObject firstMemberLink = findTestObject('Page_Member List/Member Table/First Member Name link');

ActionHandler.Handle({
	MemberListPage page = new MemberListPage()
	page.go();
	page.initFilters(practiceProfile)
	
	page.applyNameFilters(model)
	
	// submit the filter info and click on the first entry that appear
	WebUI.click(findTestObject('Object Repository/Page_Zoho shared repository/CRM pages/Person List pages/Filter Sidebar/Apply Filter button'))
	
	page.waitForTableLoad()
	
	WebUI.verifyElementPresent(firstMemberLink, 2)
	
}, { boolean success, _ ->  
	if (success) { 
		WebUI.click(firstMemberLink)
		
		WebUI.waitForPageLoad(5)
		
		return;
	}

	SMDTestCaseUtils.ConvertMemberLead({
		return new MemberLeadFactory().createMainFamilyMemberLead();
	}, 
	model,
	practiceProfile,
	)
}, TimeUnit.MINUTES.toSeconds(6))


// we should land on the member page for that member

WebUI.waitForElementPresent(findTestObject('Page_Member/First Row/Member Name') , 5)

// do verifications

final String fullName = SMDStringUtils.GetFullName(model.getFirstName(), model.getLastName()) 

WebUI.verifyElementText(findTestObject('Page_Member/First Row/Member Name') , 
	fullName)

WebUI.verifyElementText(findTestObject('Page_Member/Third Row/Personal Details/Member Name') ,
	fullName)

WebUI.scrollToElement(findTestObject('Page_Member/Fourth Row/Fee Details/Base Payment Frequency Discount amount'), 2)

WebUI.verifyElementText(findTestObject('Page_Member/Fourth Row/Fee Details/Practice Fee amount - Annual'),
	SMDNumberUtils.ToCurrencyString(model.getFee()))

WebUI.verifyElementText(findTestObject('Page_Member/Fourth Row/Fee Details/Base Payment Frequency Discount amount'),
	SMDNumberUtils.ToCurrencyString(model.getFrequencyDiscount()))

WebUI.verifyElementText(findTestObject('Page_Member/Fourth Row/Fee Details/Base Installment Fee amount'),
	SMDNumberUtils.ToCurrencyString(model.getGrossInstallment()))

WebUI.verifyElementText(findTestObject('Page_Member/Fourth Row/Fee Details/Net Annual amount'),
		SMDNumberUtils.ToCurrencyString(model.getNetAmount()))

WebUI.verifyElementText(findTestObject('Page_Member/Fourth Row/Fee Details/Net Installment amount'),
		SMDNumberUtils.ToCurrencyString(model.getNetInstallment()))