POM vs Page Factory in Selenium: Key Differences Explained
POM vs Page Factory in Selenium: Key Differences Explained
As Selenium test suites grow, poor structure becomes a bigger risk than missing coverage. Tests that directly manipulate locators and page structure tend to fail frequently, are difficult to refactor, and slow down development velocity.
Page Object Model (POM) is a design pattern that addresses these issues by enforcing separation of concerns between test logic and UI interaction logic.
This article explains how POM works in real Selenium projects, why it matters, and how to implement it correctly.
What Is Page Object Model (POM) in Selenium?
In Selenium, Page Object Model represents each logical page or major UI component as a class. That class exposes methods corresponding to user actions, such as submitting a form or navigating to a section.
A page object typically contains:
- Element locators
- Synchronization logic (waits)
- Methods representing user interactions
Tests interact with page objects exclusively and never access locators directly. This abstraction ensures tests remain stable even when the underlying UI changes.
Why Use Page Object Model in Selenium Automation?
The primary value of POM is change isolation. UI changes are inevitable, but widespread test failures should not be.
Without POM, even small UI updates require modifying dozens of tests. With POM, updates are confined to a single page object. This dramatically reduces maintenance effort and prevents brittle test suites.
POM also improves readability. Tests written using page objects resemble business workflows instead of DOM scripts, making intent clear to both developers and testers.
Problems with Traditional Selenium Test Design
Traditional Selenium tests often embed element locators and interaction logic directly inside test cases. This creates several systemic issues:
- Locators duplicated across multiple tests
- Tight coupling between tests and DOM structure
- Poor readability and unclear intent
- High cost of UI refactors
Example of fragile test design:
driver.find_element(By.ID, "email").send_keys("[email protected]") driver.find_element(By.ID, "password").send_keys("password") driver.find_element(By.ID, "login").click()
Any change to the login form forces updates across all tests using this flow.
How Page Object Model Solves Selenium Maintenance Issues
POM centralizes UI knowledge into page objects. Tests no longer care how a page is implemented; they only care what actions are possible.
If the login button selector changes, only the login page object needs updating. Tests remain untouched. This design dramatically reduces regression risk and allows UI refactoring without breaking automation.
Core Principles of Page Object Model in Selenium
A successful POM implementation follows strict principles:
- Page objects expose behavior, not elements
- Page objects do not contain assertions
- Synchronization is handled inside page objects
- One page object represents one logical page or component
Ignoring these principles leads to bloated or ineffective page objects that offer little value.
Page Object Model Architecture in Selenium
A scalable Selenium POM architecture typically includes:
- Page classes for UI interaction
- Test classes for workflows and assertions
- Base classes for setup, teardown, and shared utilities
This structure enforces clean separation of responsibilities and supports parallel execution and CI integration.
Creating a Page Object Class in Selenium
A page object defines locators and exposes methods that represent user actions.
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: EMAIL = (By.NAME, "email") PASSWORD = (By.NAME, "password") SUBMIT = (By.CSS_SELECTOR, "button[type='submit']") def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def login(self, email, password): self.wait.until(EC.visibility_of_element_located(self.EMAIL)).send_keys(email) self.driver.find_element(*self.PASSWORD).send_keys(password) self.driver.find_element(*self.SUBMIT).click()
This class hides locator complexity and timing concerns from tests.
Writing Test Classes Using Page Object Model
Test classes are where Page Object Model delivers its real value. In a well-designed Selenium POM setup, test classes describe user workflows and expectations, not browser mechanics or DOM structure. Their responsibility is to orchestrate page objects and validate outcomes, keeping test intent clear and maintenance low.
Before writing test classes, it is important to understand that tests should never know how the UI is implemented. They should only know what actions are possible and what results are expected.
Role of test classes in POM
Test classes act as consumers of page objects. They:
- Call page object methods to perform actions
- Assert expected application behavior
- Define test scenarios and edge cases
They should not:
- Contain locators
- Handle waits or retries
- Manipulate raw WebElements
This separation ensures that UI changes do not ripple through test logic.
Basic structure of a Selenium POM test class
A typical test class follows a simple flow:
- Initialize required page objects
- Perform actions using page object methods
- Validate outcomes using assertions
Example using Selenium Python with pytest-style tests:
def test_valid_login(driver): login_page = LoginPage(driver) login_page.login("[email protected]", "password123") assert "Dashboard" in driver.title
This test clearly communicates intent: a valid user logs in and reaches the dashboard. The test does not care how fields are located or how long elements take to load.
Keeping tests readable and intent-driven
Good POM-based tests read like product requirements, not automation scripts. Compare the difference:
Bad test design:
driver.find_element(By.NAME, "email").send_keys("[email protected]") driver.find_element(By.NAME, "password").send_keys("password123") driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click() assert "Dashboard" in driver.title
POM-based test design:
login_page.login("[email protected]", "password123") assert dashboard_page.is_loaded()
The second example communicates behavior rather than implementation, which makes tests easier to review and maintain.
Using multiple page objects in a single test
Real-world test flows often span multiple pages. POM makes these flows explicit and modular.
def test_user_checkout_flow(driver): LoginPage(driver).login("[email protected]", "password") ProductPage(driver).add_item_to_cart("sku-123") CheckoutPage(driver).complete_checkout() assert ConfirmationPage(driver).order_successful()
Each page object handles its own logic, while the test defines the business flow.
Where assertions belong in POM
Assertions should always live in test classes, not inside page objects. Page objects provide capabilities, not verdicts.
Good assertion placement:
assert dashboard_page.is_loaded() assert confirmation_page.order_successful()
Avoid embedding assertions like this inside page objects:
def login(self): assert "Dashboard" in self.driver.title
Embedding assertions reduces reusability and tightly couples page objects to specific tests.
Handling test setup and teardown
Test setup and teardown should be handled by the test framework, not page objects. Fixtures or setup methods prepare the browser state, while page objects assume a ready driver.
Example using a fixture:
@pytest.fixture def driver(): driver = webdriver.Chrome() yield driver driver.quit()
This keeps page objects focused solely on UI behavior.
Example: Implementing Page Object Model in Selenium
Complex workflows are built by composing multiple page objects.
class DashboardPage: def __init__(self, driver): self.driver = driver def is_loaded(self): return "Dashboard" in self.driver.title def test_login_flow(driver): LoginPage(driver).login("[email protected]", "password") assert DashboardPage(driver).is_loaded()
Each page object owns its behavior, making workflows modular and reusable.
Managing Locators in Page Object Model
Managing locators effectively is critical to making Page Object Model work as intended. In POM, locators are part of the page contract that defines how automation interacts with the UI. Poor locator practices undermine the benefits of POM by reintroducing duplication, fragility, and high maintenance cost.
Centralizing locators within page objects
All locators for a page should be defined inside the corresponding page object class and never in test classes. This ensures that UI changes affect only the page object, not the tests that depend on it.
class LoginPage: EMAIL = (By.NAME, "email") PASSWORD = (By.NAME, "password") SUBMIT = (By.CSS_SELECTOR, "button[type='submit']")
Centralization keeps locator updates localized and prevents duplication across the test suite.
Using locator tuples for consistency
Defining locators as (By, value) tuples creates a consistent pattern that works seamlessly with Selenium’s APIs.
self.driver.find_element(*self.EMAIL)This approach improves readability and simplifies refactoring when selectors change.
Choosing stable locator strategies
Not all locator strategies are equally reliable. In Page Object Model, locators should reflect user intent rather than DOM structure.
Prefer:
- data-testid or data-qa attributes
- Semantic attributes such as name or aria-label
- IDs only when explicitly guaranteed stable
Avoid:
- Positional selectors such as :nth-child
- Generated or hashed class names
- Deep DOM traversal paths
(By.CSS_SELECTOR, "button[data-testid='login-submit']")
Scoping locators to stable containers
When pages contain repeated components or similar elements, locators should be scoped to a stable parent container to avoid ambiguity.
form = self.driver.find_element(By.CSS_SELECTOR, "form[data-testid='login-form']") form.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
Scoping ensures the correct element is targeted across responsive layouts and UI variants.
Separating locators from interaction logic
Locators should describe what an element is, while page methods define how it is used. Interaction logic should never be embedded in locator definitions.
def submit_login(self): self.driver.find_element(*self.SUBMIT).click()
This separation keeps page objects readable and reusable.
Handling dynamic and conditional elements
Dynamic UI state should not be encoded into selectors. Instead of selecting elements based on transient classes or states, use stable locators and handle state changes through waits or assertions.
This keeps locators deterministic and reduces flakiness caused by timing and rendering differences.
Handling Page Actions vs Assertions in POM
One of the most important—and most frequently violated—principles of Page Object Model is the clear separation between page actions and assertions. When this boundary is blurred, page objects
become rigid, test-specific, and difficult to reuse. Correct separation ensures that POM remains scalable and adaptable as test coverage and application complexity grow.
What page actions represent in POM
Page actions are methods on a page object that model what a user can do on that page. These methods encapsulate:
- Element interactions (clicks, typing, selections)
- Synchronization logic (waits for visibility, clickability, navigation)
- Page-specific workflows
A page action should perform an operation and return control to the test without making judgments about correctness.
class LoginPage: def login(self, email, password): self.enter_email(email) self.enter_password(password) self.submit()
The page object exposes capabilities, not outcomes.
Why assertions do not belong in page objects
Assertions answer the question: Did the application behave as expected?
That question is contextual and varies from test to test.
Embedding assertions inside page objects creates several problems:
- Page objects become coupled to specific test scenarios
- The same page action cannot be reused in positive and negative tests
- Debugging becomes harder because failures are hidden inside page logic
Example of a poor practice:
def login(self, email, password): self.submit() assert "Dashboard" in self.driver.title
This makes the login method unusable for invalid login scenarios.
Role of test classes in handling assertions
Test classes are responsible for validating outcomes. They decide what to assert based on the scenario being tested.
def test_valid_login(driver): LoginPage(driver).login("[email protected]", "password") assert DashboardPage(driver).is_loaded() def test_invalid_login(driver): LoginPage(driver).login("[email protected]", "wrongpassword") assert LoginPage(driver).error_displayed()
The same page action supports multiple assertions without modification.
Returning state instead of asserting inside page objects
Page objects can expose state-checking methods that return values rather than asserting directly.
class DashboardPage: def is_loaded(self): return "Dashboard" in self.driver.title
This keeps page objects reusable while giving tests the information they need to validate behavior.
Handling validations tied to UI state
In some cases, page objects may need to confirm that an action completed successfully before proceeding, such as waiting for navigation or a modal to close. This is still an action concern, not an assertion.
def submit(self): self.driver.find_element(*self.SUBMIT).click() self.wait.until(EC.url_contains("/dashboard"))
Waiting for state is acceptable; asserting correctness is not.
Common mistakes when mixing actions and assertions
Frequent anti-patterns include:
- Asserting page titles or messages inside page methods
- Throwing assertion errors from page objects
- Writing “validation-heavy” page objects tied to one test flow
These patterns reduce reuse and increase maintenance cost.
Page Object Model Best Practices in Selenium
Page Object Model is most effective when it is applied with clear boundaries and consistent discipline. Without well-defined practices, page objects can quickly become bloated, tightly coupled to tests, and difficult to maintain. The following best practices help ensure that POM remains a scalable and reliable foundation for Selenium automation.
Before listing the practices, it is important to remember that page objects should act as a stable interface between tests and the UI, absorbing change without exposing implementation details.
- Design page objects around user actions rather than UI structure, so methods reflect what users do instead of how elements are arranged in the DOM.
- Keep assertions out of page objects and place them only in test classes, allowing the same page actions to be reused across multiple scenarios.
- Handle all waits and synchronization inside page object methods to ensure consistent timing behavior and reduce test flakiness.
- Use stable, intent-driven locators such as data-testid, semantic attributes, or guaranteed IDs instead of positional or style-based selectors.
- Keep page object methods small and focused on a single responsibility to improve readability and simplify debugging.
- Avoid exposing raw WebElements to test classes, and instead expose behavior or state through well-defined methods.
- Create page objects for logical pages or reusable components rather than entire user flows to keep responsibilities clear.
- Treat page objects as production code that should be reviewed, refactored, and maintained as the application evolves.
Following these best practices allows Page Object Model to deliver on its promise of cleaner tests, lower maintenance cost, and scalable Selenium automation.
Common Mistakes to Avoid When Using POM
Page Object Model can significantly improve Selenium test maintainability, but only when it is implemented correctly. Many automation suites adopt POM in structure but fail to gain its benefits due to recurring design mistakes. Recognizing and avoiding these pitfalls is essential to keeping page objects clean, reusable, and scalable.
Before reviewing the mistakes, it is important to remember that POM is a design pattern, not a file structure. Simply creating page classes does not guarantee good abstraction.
- Mixing assertions into page objects, which tightly couples page logic to specific test scenarios and prevents reuse across positive and negative flows.
- Exposing raw WebElements from page objects, encouraging tests to bypass page logic and directly interact with the DOM.
- Creating page objects per test case instead of per logical page or component, leading to duplication and fragmented UI knowledge.
- Overloading page objects with complex business logic or multi-step workflows that make them difficult to reason about and maintain.
- Using brittle locators such as positional selectors, generated class names, or deep DOM traversal paths inside page objects.
- Handling waits and synchronization in test classes rather than inside page object methods, resulting in inconsistent timing and flaky execution.
- Treating page objects as static or write-once artifacts instead of refactoring them as the UI and product behavior evolve.
- Blurring responsibilities between tests and page objects, making it unclear where actions end and validations begin.
Avoiding these common mistakes ensures that Page Object Model remains a true abstraction layer, helping Selenium automation stay readable, resilient, and adaptable as applications change.
What is Page Factory in Selenium?
Page Factory is an alternative pattern built on top of traditional Page Object Model that focuses on how elements are initialized and managed, rather than how pages are structured conceptually. It was introduced to reduce boilerplate code in page objects and centralize element initialization.
In Page Factory, elements are defined using annotations instead of explicit find_element calls. These elements are initialized once when the page object is created.
@FindBy(id = "username") WebElement username; @FindBy(id = "password") WebElement password; @FindBy(css = "button[type='submit']") WebElement loginButton;
The initialization happens through a factory method:
PageFactory.initElements(driver, this);The core idea is that element lookup is lazy. Elements are not located immediately when the page object is created, but only when they are accessed. This can reduce repetitive lookup code and make page classes appear cleaner.
However, Page Factory primarily addresses element declaration, not test design. It does not enforce separation of concerns, proper abstraction boundaries, or good synchronization practices. Those responsibilities still depend entirely on how the page object is written.
Key characteristics of Page Factory:
- Uses annotations to declare elements
- Reduces explicit locator calls
- Performs lazy element initialization
- Tightly couples page objects to WebElements
While Page Factory is supported by Selenium, it is not actively evolving and is less commonly used in modern Selenium projects, especially outside Java.
How to Implement Page Factory in a Selenium Project
Page Factory is implemented by defining web elements using @FindBy annotations inside page classes and initializing them using PageFactory.initElements(). The pattern is most commonly used in Selenium Java projects, where it reduces repetitive findElement() calls and keeps page classes compact.
Before implementation, it is important to note that Page Factory handles element initialization, not test design. Clean abstraction, waits, and assertions still need to be structured carefully.
Step 1: Add Selenium dependency
If the project uses Maven, include Selenium:
<dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.XX.X</version> </dependency>
If Gradle is used:
implementation 'org.seleniumhq.selenium:selenium-java:4.XX.X'Step 2: Create a Base Page to initialize Page Factory
Create a base class that initializes all @FindBy elements automatically when a page object is created.
import org.openqa.selenium.WebDriver; import org.openqa.selenium.support.PageFactory; public class BasePage { protected WebDriver driver; public BasePage(WebDriver driver) { this.driver = driver; PageFactory.initElements(driver, this); } }
This avoids repeating PageFactory.initElements() in every page class.
Step 3: Create a Page Object using @FindBy annotations
Define elements using @FindBy. Keep these elements private to avoid exposing WebElement directly to tests.
import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; public class LoginPage extends BasePage { @FindBy(name = "email") private WebElement emailInput; @FindBy(name = "password") private WebElement passwordInput; @FindBy(css = "button[type='submit']") private WebElement submitButton; public LoginPage(WebDriver driver) { super(driver); } public void login(String email, String password) { emailInput.clear(); emailInput.sendKeys(email); passwordInput.clear(); passwordInput.sendKeys(password); submitButton.click(); } }
This keeps locators centralized and interactions encapsulated.
Step 4: Write a test that uses the Page Factory page object
A test should call page actions and keep assertions in the test layer.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; public class LoginTest extends BaseTest { @Test public void validLoginShouldOpenDashboard() { driver.get("https://example.com/login"); LoginPage loginPage = new LoginPage(driver); loginPage.login("[email protected]", "password123"); assertTrue(driver.getTitle().contains("Dashboard")); } }
Step 5: Add explicit waits to improve stability
Page Factory does not handle waits automatically. Use WebDriverWait inside page methods for actions that depend on dynamic UI state.
import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.ExpectedConditions; import java.time.Duration; public void login(String email, String password) { WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOf(emailInput)).sendKeys(email); passwordInput.sendKeys(password); wait.until(ExpectedConditions.elementToBeClickable(submitButton)).click(); }
This reduces flakiness caused by async rendering and slow browsers.
Step 6: Decide whether to use @CacheLookup carefully
@CacheLookup caches elements after first lookup, which may speed up static pages but often causes stale element failures in modern apps.
@CacheLookup @FindBy(id = "static-logo") private WebElement logo;
Use @CacheLookup only for elements that truly never change, such as static headers on non-dynamic pages.
Page Object Model and Page Factory are often confused or used interchangeably, but they solve different problems in Selenium automation. Page Object Model is a design pattern that defines how test code should be structured, while Page Factory is a utility that simplifies how elements are declared and initialized within page objects. The table below highlights their practical differences and when each approach is appropriate.
| Aspect | Page Object Model (POM) | Page Factory |
|---|---|---|
| Primary Purpose | Defines a design pattern for structuring Selenium tests | Simplifies element declaration and initialization |
| Level of Abstraction | High-level, behavior-driven abstraction | Low-level, element-focused abstraction |
| Element Access | Elements are located inside methods | Elements are declared as class fields |
| Exposure to Tests | Tests interact with page methods, not elements | Often exposes WebElements directly |
| Synchronization Handling | Encourages waits inside page methods | No built-in synchronization support |
| Maintainability | High, changes localized to page objects | Lower in dynamic UIs due to tight coupling |
| Suitability for Dynamic UIs | Well-suited for modern, re-rendered interfaces | Prone to stale element issues |
| Language Usage | Language-agnostic across Selenium ecosystems | Primarily Java-centric |
| Learning Curve | Requires design discipline | Easy to start, harder to scale |
| Recommended Usage | Default approach for scalable Selenium automation | Optional helper for small, stable projects |
In practice, Page Object Model forms the foundation of maintainable Selenium automation, while Page Factory is best viewed as an optional convenience layer rather than a replacement for POM.
Conclusion
Page Object Model is a foundational pattern for building maintainable Selenium automation. By isolating UI interaction logic, enforcing clean boundaries, and aligning tests with user behavior, POM reduces maintenance cost and improves test reliability.
When combined with stable locators, proper synchronization, and real-browser execution, POM enables Selenium automation that scales with modern web applications rather than breaking under them.
FAQs
No, Page Factory is an extension of the Page Object Model that simplifies element initialization.
POM offers better flexibility and maintainability, while Page Factory reduces boilerplate code for element initialization.
Page Factory can improve code readability, but it does not significantly impact execution speed.
POM is preferred for large, scalable automation frameworks where maintainability and clear structure are priorities.
POM is a design pattern that separates page logic from test scripts, while Page Factory is an implementation approach that initializes web elements using annotations.
Related Articles
Automating File Uploads in Selenium in 2026
Discover how to automate file uploads in Selenium, including best practices, methods like sendKeys()...
Selenium and Java in 2026
Understand the core benefits of using Selenium with Java for automation testing. Learn how to set up...
Bypassing Cloudflare Challenges in 2026 using Selenium
Explore effective methods to bypass Cloudflare using Selenium, Puppeteer, and Playwright with ethica...