Modern QA2026ООП для тестирования
Join

Course12 Programming for QA

Foundations · Chapter 12

ООП для тестирования

Updated Jul 2026

Объектно-ориентированное программирование — доминирующая парадигма для структурирования фреймворков автоматизации тестирования. Паттерн Page Object Model, наиболее важный паттерн в автоматизации тестирования UI, построен на принципах ООП. Понимание классов, наследования и композиции необходимо для создания поддерживаемого тестового кода.

Классы для объектов страниц

Класс инкапсулирует данные (локаторы) и поведение (действия) для одной страницы или компонента. Такое разделение сохраняет тестовые файлы чистыми и локализует изменения в одном месте.

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)

Использование объектов страниц в тестах

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"

Тест читается как пользовательская история. Локаторы и взаимодействия скрыты внутри объекта страницы. Если CSS-селектор поля email изменится, вы обновите одну строку в LoginPage, а не каждый тест.

Наследование: базовые классы страниц

Когда страницы разделяют общее поведение (навигационная панель, подвал, общие ожидания), используйте базовый класс.

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)

Ловушка наследования

Глубокие иерархии наследования становятся хрупкими:

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

Когда SuperAdminPage ведёт себя неожиданно, вам приходится отслеживать четыре уровня, чтобы найти проблему. Изменения в BasePage могут сломать все дочерние классы.

Правило: держите наследование максимум на одном-двух уровнях.

Композиция: переиспользуемые компоненты

Композиция означает построение страниц из независимых, переиспользуемых компонентов вместо наследования поведения от родительских классов.

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

Использование составных объектов страниц

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

Наследование и композиция

Подход Когда использовать Пример
Наследование Страницы разделяют общее поведение (утилиты базовой страницы) class AdminPage(BasePage)
Композиция Страницы содержат переиспользуемые UI-компоненты self.nav = NavigationComponent(driver)

Предпочитайте композицию. Компоненты, которые вы подключаете к страницам, легче поддерживать, тестировать независимо и переиспользовать на разных страницах. Оставьте наследование для тонкого BasePage с общими утилитами.

Инкапсуляция: скрытие деталей реализации

Объекты страниц должны скрывать детали реализации от тестов. Тесты никогда не должны содержать локаторы, ожидания или команды драйвера.

# 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

Те же паттерны применимы в TypeScript с 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") ?? "";
    }
}

Практическое упражнение

Постройте фреймворк объектов страниц для простого интернет-магазина:

  1. Создайте BasePage с общими утилитами (ожидание, прокрутка, скриншот)
  2. Создайте NavigationComponent, используемый на нескольких страницах
  3. Создайте ProductListPage и CartPage с использованием композиции
  4. Напишите 3 теста, использующих ваши объекты страниц без единого локатора в тестовом файле
  5. Рефакторинг: измените локатор в объекте страницы и убедитесь, что никакие тесты не требуют обновления

Ключевые выводы

  • Page Object Model — наиболее важный паттерн проектирования в автоматизации тестирования UI
  • Один класс на страницу; локаторы находятся в объекте страницы, никогда в тесте
  • Методы возвращают объекты страниц для цепочечного вызова
  • Никаких проверок внутри объектов страниц — проверки принадлежат тестам
  • Предпочитайте композицию наследованию для переиспользуемых компонентов
  • Держите наследование на одном-двух уровнях; используйте BasePage для общих утилит