Using the Page Object Model pattern in Selenium + TestNG tests

After having introduced the Selenium + TestNG combination in my previous post, I would like to show you how to apply the Page Object Model, an often used method for improving maintainability of Selenium tests, to this setup. To do so, we need to accomplish the following steps:

  • Create Page Objects representing pages of a web application that we want to test
  • Create methods for these Page Objects that represent actions we want to perform within the pages that they represent
  • Create tests that perform these actions in the required order and performs checks that make up the test scenario
  • Run the tests as TestNG tests and inspect the results

Creating Page Objects for our test application
For this purpose, again I use the ParaBank demo application that can be found here. I’ve narrowed the scope of my tests down to just three of the pages in this application: the login page, the home page (where you end up after a successful login) and an error page (where you land after a failed login attempt). As an example, this is the code for the login page:

package com.ontestautomation.seleniumtestngpom.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginPage {
	
	private WebDriver driver;
	
	public LoginPage(WebDriver driver) {
		
		this.driver = driver;
		
		if(!driver.getTitle().equals("ParaBank | Welcome | Online Banking")) {
			driver.get("http://parabank.parasoft.com");
		}		
	}
	
	public ErrorPage incorrectLogin(String username, String password) {
		
		driver.findElement(By.name("username")).sendKeys(username);
		driver.findElement(By.name("password")).sendKeys(password);
		driver.findElement(By.xpath("//input[@value='Log In']")).click();
		return new ErrorPage(driver);
	}
	
	public HomePage correctLogin(String username, String password) {
		
		driver.findElement(By.name("username")).sendKeys(username);
		driver.findElement(By.name("password")).sendKeys(password);
		driver.findElement(By.xpath("//input[@value='Log In']")).click();
		return new HomePage(driver);
	}
}

It contains a constructor that returns a new instance of the LoginPage object as well as two methods that we can use in our tests: incorrectLogin, which sends us to the error page and correctLogin, which sends us to the home page. Likewise, I’ve constructed Page Objects for these two pages as well. A link to those implementations can be found at the end of this post.

Note that this code snippet isn’t optimized for maintainability – I used direct references to element properties rather than some sort of element-level abstraction, such as an Object Repository.

Creating methods that perform actions on the Page Objects
You’ve seen these for the login page in the code sample above. I’ve included similar methods for the other two pages. A good example can be seen in the implementation of the error page Page Object:

package com.ontestautomation.seleniumtestngpom.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class ErrorPage {
	
	private WebDriver driver;
	
	public ErrorPage(WebDriver driver) {
		
		this.driver = driver;
	}
	
	public String getErrorText() {
		
		return driver.findElement(By.className("error")).getText();
	}
}

By implementing a getErrorText method to retrieve the error message that is displayed on the error page, we can call this method in our actual test script. It is considered good practice to separate the implementation of your Page Objects from the actual assertions that are performed in your test script (separation of responsibilities). If you need to perform additional checks, just add a method that returns the actual value displayed on the screen to the associated page object and add assertions to the scripts where this check needs to be performed.

Create tests that perform the required actions and execute the required checks
Now that we have created both the page objects and the methods that we want to use for the checks in our test scripts, it’s time to create these test scripts. This is again pretty straightforward, as this example shows (imports removed for brevity):

package com.ontestautomation.seleniumtestngpom.tests;

public class TestNGPOM {
	
	WebDriver driver;
	
	@BeforeSuite
	public void setUp() {
		
		driver = new FirefoxDriver();
		driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
	}
	
	@Parameters({"username","incorrectpassword"})
	@Test(description="Performs an unsuccessful login and checks the resulting error message")
	public void testLoginNOK(String username, String incorrectpassword) {
		
		LoginPage lp = new LoginPage(driver);
		ErrorPage ep = lp.incorrectLogin(username, incorrectpassword);
		Assert.assertEquals(ep.getErrorText(), "The username and password could not be verified.");
	}
	
	@AfterSuite
	public void tearDown() {
		
		driver.quit();
	}
}

Note the use of the page objects and the check being performed using methods in these page object implementations – in this case the getErrorText method in the error page page object.

As we have designed our tests as Selenium + TestNG tests, we also need to define a testng.xml file that defines which tests we need to run and what parameter values the parameterized testLoginOK test takes. Again, see my previous post for more details.

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
 
<suite name="My first TestNG test suite" verbose="1" >
  <parameter name="username" value="john"/>
  <parameter name="password" value="demo"/>
  <test name="Login tests">
    <packages>
      <package name="com.ontestautomation.seleniumtestngpom.tests" />
   </packages>
 </test>
</suite>

Run the tests as TestNG tests and inspect the results
Finally, we can run our tests again by right-clicking on the testng.xml file in the Package Explorer and selecting ‘Run As > TestNG Suite’. After test execution has finished, the test results will appear in the ‘Results of running suite’ tab in Eclipse. Again, please note that using meaningful names for tests and test suites in the testng.xml file make these results much easier to read and interpret.

TestNG test results in Eclipse

An extended HTML report can be found in the test-output subdirectory of your project:

TestNG HTML test results

The Eclipse project I have used for the example described in this post, including a sample HTML report as generated by TestNG, can be downloaded here.

33 thoughts on “Using the Page Object Model pattern in Selenium + TestNG tests

    • Hi Ganajan,

      you’re welcome. It was a good learning experience for me as well, until recently I’ve never worked with TestNG myself.. I’m sure there’s lots left to learn for me too, so expect more TestNG-related posts in the future.

  1. Thanks Bas for the article. Isn’t it a good practice to not have page object return another page’s object. I would rather have all page objects that navigates to another page return a navigation object. What are your thoughts?

    • Hi Jaypal,

      interesting thought. Do you have an example somewhere? Could be a link to a webpage, but you can also send me a piece of code at bas@ontestautomation.com. I’d be happy to take a look, I’m always open to alternatives, especially if they’re better than what I’m doing at the moment ๐Ÿ™‚

  2. Hi Bas eventhough i did not get the whole idea i have understood some.page objects are homepage,loginpage and error page.
    The code which makes them pageobjects are as follows:
    public ErrorPage incorrectLogin(String username, String password),
    public HomePage correctLogin(String username, String password),
    public LoginPage logOut().
    correct?
    But i am confused with two function calls,
    1)LoginPage lp = new LoginPage(driver);
    2)HomePage hp = lp.correctLogin(username, password);
    what happens here?

    • I mean usually it should be,
      HomePage hp=new HomePage(); right?,
      But how does the below code,
      HomePage hp = lp.correctLogin(username, password);
      works?Interface or something?

      • Hi Sharin,

        the correctLogin() method performs a successful login on the website (that is, if all goes well). This login results in the user being redirected to the Homepage. Therefore, the correctLogin() method on the LoginPage should return a HomePage object.

        See also the example on the Selenium WebDriver webpage here.

  3. Hi Bas i have an idea.if i want to view the report of this script in extent reports after running,i believe i only have to modify your TestNGPOM.java file right?Any way i will give it a try and if it works let you know ok?

    • Hi Sherin,

      of course you can also generate an ExtentReports report in your Selenium + TestNG test. Please go ahead and try for yourself!

      • Sorry Bas,i know i am disturbing you.since there is no main() here,where will input the extent report code?
        This time can you please modify your TestNGPOM.java file to see the output file in Extent and sent to sherinchelad@gmail.com.

        • Hi Sherin,

          there is no main() method in the code sample as they are TestNG tests. TestNG uses the @Test annotation to identify test methods and when you run a class containing TestNG tests, TestNG will run every method that is annotated with @Test as a separate test.

  4. Facing an issue while executing the selenium code.Please help me in resolving this issue.Iam able to get the desired outout but with some exceptions.
    I have implemented Page object model using Test NG.

    Below is the Object page

    package excelsoft.cms.object.repository;

    import java.util.concurrent.TimeUnit;

    import javax.naming.directory.NoSuchAttributeException;

    import junit.framework.Assert;
    import junit.framework.ComparisonFailure;

    import org.junit.Before;
    import org.openqa.selenium.By;
    import org.openqa.selenium.NoSuchElementException;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.firefox.FirefoxDriver;
    import org.openqa.selenium.interactions.Actions;
    import org.testng.TestNGException;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.AfterTest;
    import org.testng.annotations.BeforeMethod;
    import org.testng.annotations.BeforeSuite;
    import org.testng.annotations.BeforeTest;
    import org.testng.annotations.Test;

    import excelsoft.cms.Configuration;

    public class Loginpageobjects {

    static WebElement ele;
    private static WebDriver driver;

    public Loginpageobjects(WebDriver driver) {
    this.driver = driver;
    driver.get(Configuration.URL);

    }

    public WebElement getusernameid(WebDriver driver)
    {

    ele = driver.findElement(By.id(“txtLoginName”));

    return ele;

    }

    public WebElement getpasswordid(WebDriver driver) {
    try {
    ele = driver.findElement(By.id(“txtPassword”));
    }

    catch (Exception ex) {
    ex.printStackTrace();
    System.out.println(“inside passwordobject”);

    }
    return ele;
    }

    public WebElement getloginbuttonid(WebDriver driver) {
    try {
    ele = driver.findElement(By.id(“BtnLogin”));
    } catch (Exception ex) {
    ex.printStackTrace();
    System.out.println(“inside getloginbutton id”);

    }
    return ele;
    }

    public WebElement getloginerrormsgid(WebDriver driver) {
    try {
    ele = driver.findElement(By.id(“spnErrorMSG”));
    } catch (Exception ex) {
    ex.printStackTrace();

    }
    return ele;
    }

    }

    Below is the action page.

    package excelsoft.cms;

    import java.io.File;
    import java.util.concurrent.TimeUnit;

    import junit.framework.Assert;

    import org.apache.commons.io.FileUtils;
    import org.openqa.selenium.OutputType;
    import org.openqa.selenium.TakesScreenshot;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.firefox.FirefoxDriver;
    import org.openqa.selenium.remote.UnreachableBrowserException;
    import org.testng.annotations.AfterClass;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeSuite;
    import org.testng.annotations.Test;

    //import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptExecutor;
    //import com.gargoylesoftware.htmlunit.javascript.host.file.File;
    //import com.gargoylesoftware.htmlunit.javascript.host.file.File;
    //import com.sun.jna.platform.FileUtils;

    import excelsoft.cms.object.repository.Loginpageobjects;

    @SuppressWarnings(“deprecation”)
    public class Login

    {
    String currenturl;
    WebDriver driver;
    Loginpageobjects loginpageobj;
    static WebElement logingele;
    static WebElement passwordele;
    static WebElement loginbuttonele;
    static WebElement errorele;

    @BeforeSuite
    public void setbrowser() {
    driver = new FirefoxDriver();
    loginpageobj = new Loginpageobjects(driver);
    driver.manage().window().maximize();
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);

    }

    public void getelerefer()
    {
    try
    {
    System.out.println(“inside getrefer”);
    logingele = loginpageobj.getusernameid(driver);

    passwordele=loginpageobj.getpasswordid(driver);
    loginbuttonele =loginpageobj.getloginbuttonid(driver);
    //errorele = loginpageobj.getloginerrormsgid(driver);
    }

    catch(UnreachableBrowserException ex)

    {

    ex.printStackTrace();

    }
    }

    @Test
    public void usernamedisplaytestcase() throws InterruptedException

    {

    try {
    getelerefer();
    currenturl= driver.getCurrentUrl();
    // Check the element is displayed in the page
    //if(loginpageobj.getusernameid(driver) != null)
    if(logingele !=null)
    {
    logingele.click();
    Assert.assertNotNull(logingele
    .isDisplayed());

    }
    }

    catch (Exception ex) {

    ex.printStackTrace();

    }
    }

    @Test
    public void passworddisplaytestcase() throws InterruptedException

    {

    try {
    getelerefer();
    if(passwordele != null)
    {
    // Check the element is displayed in the page
    Assert.assertNotNull(passwordele
    .isDisplayed());

    }
    }

    catch (Exception ex) {

    }
    }

    @Test
    public void buttondisplaytestcase() throws InterruptedException

    {

    try {
    getelerefer();
    if(loginbuttonele!=null)
    {
    // Check the element is displayed in the page
    Assert.assertNotNull(loginbuttonele
    .isDisplayed());

    File scrFile =(File) ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
    //The below method will save the screen shot in d drive with name “screenshot.png”
    FileUtils.copyFile(scrFile, new File(“D:\\loginandpasswordfielddisplay.png”));
    }
    }

    catch (Exception ex) {

    }
    }

    @Test
    public void checkerrormessagedisplay() throws InterruptedException

    {

    try {
    getelerefer();
    if(logingele!=null && passwordele!=null && loginbuttonele!=null )
    {
    logingele.sendKeys(“invaliduser”);
    passwordele.sendKeys(“invalidpassword”);
    loginbuttonele.click();
    File scrFile =(File) ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
    //The below method will save the screen shot in d drive with name “screenshot.png”
    FileUtils.copyFile(scrFile, new File(“D:\\errormessage.png”));
    Assert.assertNotNull(loginpageobj.getloginerrormsgid(driver)
    .isDisplayed());

    }
    }

    catch (Exception ex) {

    System.out.println(“inside catch error message check”);
    ex.printStackTrace();

    }
    }

    @Test
    public void checkerrormessagetext() throws InterruptedException

    {

    try {
    getelerefer();
    if(logingele!=null && passwordele!=null && loginbuttonele!=null )
    {
    logingele.clear();
    passwordele.clear();
    logingele.sendKeys(“invaliduser”);
    passwordele.sendKeys(“invalidpassword”);
    loginbuttonele.click();
    String gettext = loginpageobj.getloginerrormsgid(driver).getText();

    Assert.assertEquals(“Invalid Login Name and Password”, gettext);
    }

    }

    catch (Exception ex) {

    }
    }

    @Test
    public void checksuccessfulllogin() {
    try {
    getelerefer();
    if(loginpageobj.getusernameid(driver)!=null && loginpageobj.getpasswordid(driver)!=null && loginpageobj.getloginbuttonid(driver)!=null )
    {
    logingele.clear();
    passwordele.clear();

    logingele.sendKeys(“sarasadmin”);
    passwordele.sendKeys(“sarasadmin”);

    loginbuttonele.click();
    String homepageurl = driver.getCurrentUrl();

    Assert.assertEquals(
    “http://192.168.5.214/SarasCMS/AdminDashboard.aspx”,
    homepageurl);

    Thread.sleep(5000);

    File scrFile =(File) ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
    //The below method will save the screen shot in d drive with name “screenshot.png”
    FileUtils.copyFile(scrFile, new File(“D:\\homepage.png”));
    }
    //driver.switchTo().defaultContent();
    //driver.close();
    //System.exit(0);
    }

    catch (Exception ex) {

    System.out.println(“inside catch”);
    ex.printStackTrace();

    }

    }

    @AfterClass
    public void browserend()
    {
    driver.close();
    }
    }

    Please try executing the code by replacing the element ids to the facebook or some other website which u would like to and help me out iam stuck here ๐Ÿ™

    • Sure you can, those two are completely independent. There are lots of examples on how to configure listeners in your TestNG.xml file, including one example in a blog post I wrote on using ExtentReports as a TestNG listener. You should be able to find it using the search at the top right of this page.

  5. Hi Bas,

    Below are my POM and TestNG classes. If i extend to BaseTest class where i wrote pre and post conditions, Two browsers are opening. Also when i use actions class it throws NoSuchElementException. I have also tried by clicking on the webelements but in that case i get NullPointer exception.

    POM class:
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.openqa.selenium.interactions.Actions;
    import org.openqa.selenium.support.FindBy;
    import org.openqa.selenium.support.PageFactory;

    public class CourseCreatePage {

    @FindBy(xpath=(“(//li//a)[1]” ))
    private WebElement add_content;

    @FindBy(xpath=(“(//li//a)[1]” ))
    private WebElement add_chap ;

    public CourseCreatePage(WebDriver driver)
    {
    PageFactory.initElements(driver, this);
    }

    public void addContent()
    {
    add_content.click();
    add_chap.click();

    }
    }

    TestNG Class:
    package scripts;

    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.testng.annotations.Test;

    import POM.CourseCreatePage;

    public class CreateCourse
    {

    WebDriver driver1;

    @Test
    public void course()
    {
    CourseCreatePage obj2 = new CourseCreatePage(driver1);
    obj2.addContent();
    }
    }

    BaseTest Class:
    package scripts;

    import java.util.concurrent.TimeUnit;

    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.testng.annotations.AfterClass;
    import org.testng.annotations.AfterSuite;
    import org.testng.annotations.BeforeClass;
    import org.testng.annotations.BeforeSuite;

    public class BaseTest {
    public WebDriver driver;

    @BeforeClass
    public void precondition()
    {
    System.setProperty(“webdriver.chrome.driver”, “F:\\Chrome\\chromedriver.exe”);
    driver = new ChromeDriver();
    driver.get(“http://admin.staging.aakashitutor.com/user/login”);
    driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    }

    /*@AfterClass
    public void postCondition()
    {
    driver.close();
    }*/
    }

    • Hey Deepak, you know you can debug your tests like any other piece of software you’re running? That’ll probably give you all the information you need to get this to work.

  6. I wish to know how to organize manual test cases in POM.
    Should one test case be in one .java file ?
    OR should we have one test case under every @Test annotation so we have one java file containing multiple test cases ?

    • I don’t know what your question has to do with Maven POM files, but yes, I usually group together logical sets of tests (each annotated with @Test) in a .java file. There is no one right way to do this, though.

      • Hey sorry for confusion, POM is page object model ๐Ÿ™‚
        So if I have a product with say 10 menu items on the left of the home page, each menu item having multiple functionalities like

        Menu A having: create person, create company, search them, attach something to a person or company & detach something from them

        Menu B having: Remove person , remove company

        Menu C having : make some transaction related to that person/company, sell something to them……& so on…

        So would it be better to make one java file for each Menu ? like MenuA.java , MenuB.java, MenuC.java & each of these .java files having multiple @Test’s , one @Test for create person, one @Test for create company….& so on.

        OR would it be better to have java files one for each functionality like createPerson.java , one for createCompany.java

        Sorry for the long read ๐Ÿ™‚

        • Hey Nikhil,

          thanks for clarifying and sorry for the confusion. I can’t say what would work better in your case. I think I’d go with one test file for each menu, but that doesn’t mean that’s the best option. Or that there is a best option. Just go with what works for you.

  7. Hi,

    We are trying to implement Test Automation for Sharepoint applications. Now, I have noticed that in TestNG test cases(@Test methods) all the values are hardcoded.
    For ex;
    if(!driver.getTitle().equals(“ParaBank | Welcome | Online Banking”)) {
    driver.get(“http://parabank.parasoft.com”);
    }

    The URL in the get() method has been written in the method. But what if we want to externally give the data?

    Is it only possible through parameters?

    Our xpath, URLs keep changing but test cases remain the same. Would you recommend using TestNG? Or should we go for a different framework?

    • Hey Saili,

      that was just a quick example, as you said it is better to provide URL’s, object locators and other values through some sort of parameterization. Using TestNG parameters is one option, storing them in a config file, some sort of object repo, a separate constants class, a database or even an Excel sheet are other options. There is more than one way to do this!

      • Hi, Thanks a ton for your reply. Your articles are really helpful. I’m sorry I didn’t say it earlier.

        So, to do test automation we are looking for either
        – Keyword driven framework
        – TestNG + Excel Sheet to get the data/locators

        I wanted to know what advantages I would have using TestNG over keyword driven.

        • Hi Saili,

          it’s not a matter of one over the other, you could perfectly create a keyword driven solution that uses TestNG (if only for the assertions it provides). There is no ‘best’ way to create a test automation solution. If that were the case, someone would already have created it and posted it online for everybody to use (and criticize). It is a matter of what best fits you and your team. And that is very hard to decide from a couple of comments on a blog…

          Personally, what I’ve used with success is the following setup:

          – Cucumber (or SpecFlow) for specifying the behavior and intent of your tests in human readable format
          – Selenium (with Page Objects and the LoadableComponent pattern) for interacting with the browser. Object locators defined in the Page Objects, not externally
          – NUnit/MSTest (for C#) or JUnit/TestNG (for Java) as the test runner
          – ExtentReports for human readable reporting with screenshots

          But that is just one solution that worked well for that specific client and project. That doesn’t mean it’s the best option for your situation.

          • Hi Bas,

            Thanks a lot for such a detailed response. I seem to have gotten a good idea from what you have told me.

            Thanks!

  8. A very informative article.May I ask a basic question as to why invalid password for the invalid login test in not provided in the TestNG xml file.

  9. Hello can you give me all the test cases of login and logout test cases script using POm concept,I need this ,I have made action,page and test classes

    • What do you mean by ‘all the test cases’? Here are some suggestions. If you’ve got the page objects and the driver actions implemented already, creating the actual tests should be very straightforward.

Leave a Reply

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