How to Refactor a Selenium Automation Testing Framework with POM and the Factory Pattern

For Web automation testing teams, this article presents an engineering-focused refactoring approach for Selenium-based frameworks: encapsulate pages with the Page Object Model (POM), manage browsers with the Factory Pattern, standardize logging and failure screenshots, and integrate Pytest and Allure to improve regression efficiency. The core pain points are brittle scripts, high maintenance costs, and poor extensibility. Keywords: Selenium, POM, test automation.

Technical Specifications at a Glance

Parameter Description
Core language Python
Automation protocol WebDriver
Applicable scenarios Web UI automation, regression testing, cross-browser validation
Source article popularity The original article shows approximately 213 views
Core dependencies selenium, pytest, allure-pytest, logging
Key patterns POM, Factory, Fixture

Traditional Selenium Scripts Require an Engineering-Grade Refactor

Selenium is the de facto standard for Web automation testing, but many teams still operate with a “just make the script run” mindset. The usual result is hard-coded locators, mixed business logic and driver logic, and a full batch of broken test cases whenever the page changes slightly.

The bigger issue is not a single failure, but the cost of troubleshooting after failure. Without standardized logs, screenshots, exception handling, and directory structure, regression testing gradually turns into a high-cost manual debugging process.

The Original Approach Is Hard to Maintain

from selenium import webdriver
from selenium.webdriver.common.by import By

# Create the driver directly in the test, which mixes responsibilities
driver = webdriver.Chrome()
driver.get("https://example.com/login")

# Hard-coded locators break easily when the page changes
driver.find_element(By.ID, "username").send_keys("test")
driver.find_element(By.ID, "password").send_keys("123456")
driver.find_element(By.ID, "submit").click()

This code can complete a login flow, but it cannot support long-term maintenance. The core problem is the lack of an abstraction layer.

Refactoring the Framework Around POM and the Factory Pattern Is the Recommended Approach

The value of POM goes far beyond simply “writing pages as classes.” It separates locator management, page behaviors, and assertion entry points into distinct layers. As a result, changes stay inside the page object instead of dragging down all test cases.

The Factory Pattern solves browser instantiation. Browser type, startup options, headless mode, and driver compatibility should all be handled centrally rather than scattered across every test file.

Page Objects Should Encapsulate Page Behaviors

from selenium.webdriver.common.by import By

class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.username_input = (By.ID, "username")
        self.password_input = (By.ID, "password")
        self.submit_button = (By.ID, "submit")

    def enter_username(self, username):
        # Clear first, then enter text to avoid dirty data affecting the test
        self.driver.find_element(*self.username_input).clear()
        self.driver.find_element(*self.username_input).send_keys(username)

    def enter_password(self, password):
        # Access elements through locator tuples consistently to reduce repeated strings
        self.driver.find_element(*self.password_input).clear()
        self.driver.find_element(*self.password_input).send_keys(password)

    def click_submit(self):
        # Once page actions are encapsulated, test cases only express business intent
        self.driver.find_element(*self.submit_button).click()

This code encapsulates locators and operations inside a page class, so the test layer focuses only on what to do, not how to find elements.

The Browser Factory Should Centralize Driver Creation

from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions

class BrowserFactory:
    @staticmethod
    def create_browser(browser_name="chrome"):
        # Normalize to lowercase to avoid branch mismatches caused by config differences
        name = browser_name.lower()

        if name == "chrome":
            options = ChromeOptions()
            options.add_argument("--no-sandbox")  # Supports some containerized environments
            options.add_argument("--disable-dev-shm-usage")  # Reduces shared memory issues
            return webdriver.Chrome(options=options)

        if name == "firefox":
            options = FirefoxOptions()
            options.add_argument("--headless")  # Runs without a UI, suitable for CI
            return webdriver.Firefox(options=options)

        raise ValueError(f"Unsupported browser: {browser_name}")

This factory code standardizes driver initialization and makes it easier to integrate configuration files, command-line arguments, and CI pipelines.

A Complete Execution Flow Must Include Logging, Assertions, and Failure Screenshots

Clicking and typing alone do not make a test. A reliable test must include assertions, error recording, and retained evidence. Failure screenshots are the lowest-cost way to preserve the scene, while logs connect the execution path with the exception context.

A Practical Main Flow for Login Testing

import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def run_login_test(browser_type="chrome"):
    driver = BrowserFactory.create_browser(browser_type)
    try:
        driver.get("https://example.com/login")  # Navigate to the page under test first
        page = LoginPage(driver)
        page.enter_username("valid_user")
        page.enter_password("valid_pass")
        page.click_submit()

        # Perform a basic assertion based on the URL change
        assert "dashboard" in driver.current_url, "Did not redirect to the dashboard page after login"
        logger.info("Login test passed")
    except Exception as e:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        screenshot = f"screenshots/fail_{timestamp}.png"
        driver.save_screenshot(screenshot)  # Automatically capture a screenshot after failure
        logger.error(f"Test failed: {e}; Screenshot saved at: {screenshot}")
        raise
    finally:
        driver.quit()  # Always release resources

This flow connects the driver, page object, assertions, logs, and screenshots into a closed loop, forming the minimum viable unit of an engineering-grade framework.

Team-Level Benefits Only Appear After Integrating Pytest and Allure

Single-file scripts are fine for demos, but not for collaboration. In a team environment, browser lifecycle management should belong to fixtures, result presentation should belong to Allure, and scheduling should belong to Jenkins or GitHub Actions.

Typical placement of an Allure test report in a pipeline AI Visual Insight: In the original article, this image is closer to a platform run-entry icon than a real report screenshot. It is best understood as a placeholder for the automated execution entry point, representing the starting point of the test flow from code execution to result generation to report viewing. It does not contain a concrete metrics dashboard, so it should not be used as evidence of report quality.

Fixtures Should Manage Browser Lifecycle

import pytest

@pytest.fixture(scope="function")
def browser():
    # Create a fresh driver for each test case to avoid state contamination
    driver = BrowserFactory.create_browser("chrome")
    yield driver
    driver.quit()  # Release resources consistently after each test case

def test_valid_login(browser):
    browser.get("https://example.com/login")
    page = LoginPage(browser)
    page.enter_username("admin")
    page.enter_password("password123")
    page.click_submit()

    # Assert the final business outcome instead of only asserting that a click succeeded
    assert "welcome" in browser.current_url

This code separates resource management from test logic, making large-scale regression runs more stable and easier to extend.

The Recommended Directory Structure Directly Affects Future Scalability

project/
├── tests/
│   ├── test_login.py
│   └── test_logout.py
├── pages/
│   └── login_page.py
├── utils/
│   ├── browser_factory.py
│   └── helpers.py
├── reports/
│   └── allure-results/
└── screenshots/

This layout stores tests, pages, utilities, reports, and evidence in separate layers, making it well suited to evolve into a team-standard framework.

The Core Benefit of This Refactor Is Lower Long-Term Maintenance Cost

The real gain from moving from raw scripts to an engineering-grade framework is not that the code looks cleaner. It is that page changes require updates in one place, browser switching requires only configuration changes, and failure analysis starts directly from screenshots and logs. That is the kind of framework that can sustain continuous regression testing.

If you further integrate scheduled jobs, parallel execution, and report notifications, this solution becomes more than a test automation tool. It becomes quality assurance infrastructure.

FAQ

1. Why does POM significantly reduce maintenance costs?

Because page element locators and page behaviors are encapsulated in one place. When the UI changes, you usually only need to update the page object instead of revising test cases one by one.

2. What is the most important value of the Factory Pattern in a Selenium framework?

It centralizes browser creation logic, making it easier to manage parameter differences across Chrome, Firefox, and Edge, and easier to integrate with CI and command-line configuration.

3. Can I keep only one of logging, screenshots, and Allure?

That is not recommended. Logs capture the process, screenshots preserve on-site evidence, and Allure aggregates and visualizes results. You need all three to build a complete troubleshooting chain.

Core Summary

This article refactors traditional Selenium scripts into an engineering-grade automation testing framework centered on POM, a Browser Factory, logging and screenshots, Pytest, and Allure. It addresses brittle scripts, high coupling, and complex cross-browser configuration, and provides a directory structure and code patterns that teams can apply directly.