Selenium Page Object Model (POM) with TestNG & Java: A Guide
In modern test automation, maintainability and scalability matter as much as execution speed. As applications grow, poorly structured Selenium tests quickly turn into fragile scripts. The combination of Selenium WebDriver, Page Object Model (POM), TestNG, and Java provides a clean and scalable automation architecture.
In this guide, you will learn how to build a complete Selenium Page Object Model framework using TestNG and Java with a real, working example.
Understanding the Core Components
What is Page Object Model (POM)?
Page Object Model is a design pattern where each web page is represented as a Java class. All page elements and actions are encapsulated inside that class. If UI changes, only the page class needs updating, not every test.
Why Selenium WebDriver?
Selenium WebDriver allows direct browser automation using real user interactions. It supports all major browsers and integrates well with Java-based test frameworks.
Why TestNG?
- Annotation-based lifecycle management
- Parallel test execution
- Data-driven testing using DataProviders
- Rich HTML reports
Project Structure
src/main/java
└── com/example/automation/pages
├── BasePage.java
├── LoginPage.java
└── HomePage.java
src/test/java
└── com/example/automation/tests
└── LoginTest.java
pom.xml
testng.xml
Creating the Base Page Class
The BasePage class handles WebDriver setup, teardown, and common functionality.
// BasePage.java
package com.example.automation.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.PageFactory;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.time.Duration;
public class BasePage {
protected WebDriver driver;
public BasePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void setUp() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
}
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
public void navigateTo(String url) {
driver.get(url);
}
}
Designing Page Object Classes
LoginPage.java
// LoginPage.java
package com.example.automation.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class LoginPage extends BasePage {
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(id = "flash")
private WebElement flashMessage;
public LoginPage(WebDriver driver) {
super(driver);
}
public HomePage login(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
return new HomePage(driver);
}
public LoginPage loginWithInvalidCredentials(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
return this;
}
public String getFlashMessageText() {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOf(flashMessage));
return flashMessage.getText();
}
public boolean isLoginButtonDisplayed() {
return loginButton.isDisplayed();
}
}
HomePage.java
// HomePage.java
package com.example.automation.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class HomePage extends BasePage {
@FindBy(className = "icon-signout")
private WebElement logoutButton;
@FindBy(xpath = "//h2[contains(text(),'Secure Area')]")
private WebElement secureAreaHeader;
public HomePage(WebDriver driver) {
super(driver);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOf(secureAreaHeader));
}
public boolean isLogoutButtonDisplayed() {
return logoutButton.isDisplayed();
}
public String getSecureAreaHeaderText() {
return secureAreaHeader.getText();
}
public LoginPage logout() {
logoutButton.click();
return new LoginPage(driver);
}
}
Creating TestNG Test Class
LoginTest.java
// LoginTest.java
package com.example.automation.tests;
import com.example.automation.pages.HomePage;
import com.example.automation.pages.LoginPage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.*;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.time.Duration;
public class LoginTest {
private WebDriver driver;
private LoginPage loginPage;
private String baseUrl = "http://the-internet.herokuapp.com/login";
@BeforeMethod
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.get(baseUrl);
loginPage = new LoginPage(driver);
}
@Test
public void testSuccessfulLogin() {
HomePage homePage = loginPage.login("tomsmith", "SuperSecretPassword!");
Assert.assertTrue(homePage.isLogoutButtonDisplayed());
Assert.assertEquals(homePage.getSecureAreaHeaderText(), "Secure Area");
homePage.logout();
}
@Test
public void testInvalidLogin() {
loginPage.loginWithInvalidCredentials("wrong", "wrong");
Assert.assertTrue(loginPage.getFlashMessageText().contains("invalid"));
}
@AfterMethod
public void tearDown() {
driver.quit();
}
}
Running Tests Using testng.xml
Best Practices
- Keep assertions only in test classes
- Use explicit waits instead of Thread.sleep()
- One page = one Page Object
- Reuse page methods across tests
Frequently Asked Questions (FAQs)
Q1: What is the main advantage of using the Page Object Model?
The main advantage of the Page Object Model is improved test maintainability and reusability. When UI elements or their locators change, only the corresponding page object class needs to be updated instead of modifying multiple test cases. This significantly reduces maintenance effort in large automation suites.
Q2: Can I use Page Object Model with frameworks other than TestNG?
Yes, the Page Object Model is a design pattern and is not limited to TestNG. It can be used with other testing frameworks such as JUnit, Cucumber, or even with different programming languages. TestNG is commonly preferred due to its powerful features like parallel execution and test grouping.
Q3: How can I manage multiple browser instances in a Selenium POM framework?
Multiple browser instances are usually managed using a thread-safe approach where each test thread has its own WebDriver instance. This is commonly achieved by using a ThreadLocal-based driver manager, which ensures isolated and conflict-free parallel test execution.
Q4: When should I avoid using the Page Object Model?
Page Object Model may be unnecessary for very small projects with only a few pages and minimal test cases. In such scenarios, the overhead of creating separate page classes might outweigh the benefits. However, for scalable or long-term projects, POM is strongly recommended.
Q5: What is the difference between Page Object Model and the Screenplay Pattern?
Page Object Model represents web pages as objects that contain UI elements and actions. The Screenplay Pattern follows an actor-based approach, where actors perform tasks and interact with UI targets. Screenplay promotes better separation of concerns and improves test readability for complex automation frameworks.
Conclusion
Using Selenium with Page Object Model, TestNG, and Java creates a clean, maintainable, and scalable automation framework. This structure is production-ready and widely used in enterprise automation projects.

