ООП для тестирования
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") ?? "";
}
}
Практическое упражнение
Постройте фреймворк объектов страниц для простого интернет-магазина:
- Создайте BasePage с общими утилитами (ожидание, прокрутка, скриншот)
- Создайте NavigationComponent, используемый на нескольких страницах
- Создайте ProductListPage и CartPage с использованием композиции
- Напишите 3 теста, использующих ваши объекты страниц без единого локатора в тестовом файле
- Рефакторинг: измените локатор в объекте страницы и убедитесь, что никакие тесты не требуют обновления
Ключевые выводы
- Page Object Model — наиболее важный паттерн проектирования в автоматизации тестирования UI
- Один класс на страницу; локаторы находятся в объекте страницы, никогда в тесте
- Методы возвращают объекты страниц для цепочечного вызова
- Никаких проверок внутри объектов страниц — проверки принадлежат тестам
- Предпочитайте композицию наследованию для переиспользуемых компонентов
- Держите наследование на одном-двух уровнях; используйте BasePage для общих утилит