QA Engineer Skills 2026QA-2026OOP for Testing

OOP for Testing

Object-Oriented Programming is the dominant paradigm for structuring test automation frameworks. The Page Object Model, the most important pattern in UI test automation, is built on OOP principles. Understanding classes, inheritance, and composition is essential for building maintainable test code.


Classes for Page Objects

A class encapsulates data (locators) and behavior (actions) for a single page or component. This separation keeps test files clean and localizes changes to one place.

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:
    URL = "/login"
    EMAIL = (By.CSS_SELECTOR, "input[name='email']")
    PASSWORD = (By.CSS_SELECTOR, "input[name='password']")
    SUBMIT = (By.CSS_SELECTOR, "button[type='submit']")
    ERROR_MESSAGE = (By.CSS_SELECTOR, ".error-message")

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def open(self):
        self.driver.get(f"{BASE_URL}{self.URL}")
        return self

    def login(self, email: str, password: str) -> "DashboardPage":
        self.driver.find_element(*self.EMAIL).send_keys(email)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.SUBMIT).click()
        return DashboardPage(self.driver)

    def get_error_message(self) -> str:
        element = self.wait.until(EC.visibility_of_element_located(self.ERROR_MESSAGE))
        return element.text

class DashboardPage:
    WELCOME_HEADER = (By.CSS_SELECTOR, "h1.welcome")
    LOGOUT_BUTTON = (By.CSS_SELECTOR, "[data-testid='logout']")

    def __init__(self, driver):
        self.driver = driver

    def get_welcome_text(self) -> str:
        return self.driver.find_element(*self.WELCOME_HEADER).text

    def logout(self) -> "LoginPage":
        self.driver.find_element(*self.LOGOUT_BUTTON).click()
        return LoginPage(self.driver)

Using Page Objects in Tests

def test_login_success(driver):
    dashboard = LoginPage(driver).open().login("user@test.com", "pass123")
    assert dashboard.get_welcome_text() == "Welcome"

def test_login_invalid_password(driver):
    login_page = LoginPage(driver).open()
    login_page.login("user@test.com", "wrongpass")
    assert login_page.get_error_message() == "Invalid email or password"

The test reads like a user story. Locators and interactions are hidden inside the page object. If the CSS selector for the email field changes, you update one line in LoginPage, not every test.


Inheritance: Base Page Classes

When pages share common behavior (navigation bar, footer, common waits), use a base class.

class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def get_page_title(self) -> str:
        return self.driver.title

    def is_loaded(self, locator) -> bool:
        try:
            self.wait.until(EC.presence_of_element_located(locator))
            return True
        except TimeoutException:
            return False

    def scroll_to_element(self, locator):
        element = self.driver.find_element(*locator)
        self.driver.execute_script("arguments[0].scrollIntoView(true);", element)

class LoginPage(BasePage):
    # Inherits __init__, get_page_title, is_loaded, scroll_to_element
    EMAIL = (By.CSS_SELECTOR, "input[name='email']")
    # ...

class AdminPage(BasePage):
    USER_TABLE = (By.CSS_SELECTOR, ".user-table")

    def is_loaded(self, locator=None):
        return super().is_loaded(locator or self.USER_TABLE)

The Inheritance Trap

Deep inheritance hierarchies become brittle:

# BAD: 4 levels deep — hard to understand and modify
class BasePage: ...
class AuthenticatedPage(BasePage): ...
class AdminPage(AuthenticatedPage): ...
class SuperAdminPage(AdminPage): ...

When SuperAdminPage behaves unexpectedly, you must trace through four levels to find the issue. Changes to BasePage can break all downstream classes.

Rule of thumb: Keep inheritance to one or two levels maximum.


Composition: Reusable Components

Composition means building pages from independent, reusable components instead of inheriting behavior from parent classes.

class NavigationComponent:
    MENU_BUTTON = (By.CSS_SELECTOR, "[data-testid='menu']")
    SEARCH_INPUT = (By.CSS_SELECTOR, "[data-testid='search']")

    def __init__(self, driver):
        self.driver = driver

    def open_menu(self):
        self.driver.find_element(*self.MENU_BUTTON).click()

    def search(self, query: str):
        self.driver.find_element(*self.SEARCH_INPUT).send_keys(query)

class NotificationComponent:
    BELL_ICON = (By.CSS_SELECTOR, "[data-testid='notifications']")
    COUNT_BADGE = (By.CSS_SELECTOR, ".notification-count")

    def __init__(self, driver):
        self.driver = driver

    def get_count(self) -> int:
        return int(self.driver.find_element(*self.COUNT_BADGE).text)

class DashboardPage:
    def __init__(self, driver):
        self.driver = driver
        self.nav = NavigationComponent(driver)         # composition
        self.notifications = NotificationComponent(driver)  # composition

    # Dashboard-specific methods
    def get_welcome_text(self) -> str:
        return self.driver.find_element(By.CSS_SELECTOR, "h1.welcome").text

Using Composed Page Objects

def test_dashboard_navigation(driver):
    dashboard = DashboardPage(driver)
    dashboard.nav.search("settings")          # uses nav component
    assert dashboard.notifications.get_count() >= 0  # uses notification component

Inheritance vs Composition

Approach When to Use Example
Inheritance Pages share common behavior (base page utilities) class AdminPage(BasePage)
Composition Pages contain reusable UI components self.nav = NavigationComponent(driver)

Prefer composition. Components that you plug into pages are easier to maintain, test independently, and reuse across different pages. Reserve inheritance for a thin BasePage with shared utilities.


Encapsulation: Hiding Implementation Details

Page objects should hide implementation details from tests. Tests should never contain locators, waits, or driver commands.

# BAD: locators and waits in the test
def test_add_to_cart(driver):
    driver.find_element(By.CSS_SELECTOR, ".product-card button.add").click()
    WebDriverWait(driver, 10).until(
        EC.text_to_be_present_in_element(
            (By.CSS_SELECTOR, ".cart-count"), "1"
        )
    )

# GOOD: all details inside the page object
def test_add_to_cart(driver):
    product_page = ProductPage(driver)
    product_page.add_first_item_to_cart()
    assert product_page.get_cart_count() == 1

TypeScript Page Objects

The same patterns apply in TypeScript with Playwright:

class LoginPage {
    constructor(private page: Page) {}

    async goto() {
        await this.page.goto("/login");
    }

    async login(email: string, password: string): Promise<DashboardPage> {
        await this.page.fill('[name="email"]', email);
        await this.page.fill('[name="password"]', password);
        await this.page.click('[type="submit"]');
        await this.page.waitForURL("/dashboard");
        return new DashboardPage(this.page);
    }

    async getErrorMessage(): Promise<string> {
        return await this.page.textContent(".error-message") ?? "";
    }
}

Practical Exercise

Build a page object framework for a simple e-commerce site:

  1. Create a BasePage with common utilities (wait, scroll, screenshot)
  2. Create a NavigationComponent used across multiple pages
  3. Create ProductListPage and CartPage using composition
  4. Write 3 tests that use your page objects without any locators in the test file
  5. Refactor: change a locator in a page object and verify no tests need updating

Key Takeaways

  • Page Object Model is the most important design pattern in UI test automation
  • One class per page; locators live in the page object, never in the test
  • Methods return page objects for fluent chaining
  • No assertions inside page objects — assertions belong in tests
  • Prefer composition over inheritance for reusable components
  • Keep inheritance to one or two levels; use BasePage for shared utilities