📖 Теория: Summary session 6 — POM
⚡ POM за 2 минуты
POM = каждая страница → класс. Локаторы и действия — в классе. Тест вызывает методы.
BasePage(driver)→find(locator),click(locator),type_text(locator, text)LoginPage(BasePage)→USERNAME_INPUT = (By.ID, "user-name")+ методыBaseTest→@pytest.fixture(scope="class", autouse=True)→ все тесты класса получаютself.driver,self.login_page- При изменении UI правим только класс страницы, тесты не трогаем
Почему POM: проблема без паттерна
Из лекции: когда тестов несколько, написание локаторов прямо в каждом тесте создаёт значительные минусы:
# tests/test_login_bad.py
# Проблема: локаторы дублируются в КАЖДОМ тесте
def test_valid_login(driver):
driver.find_element(By.ID, "user-name").send_keys("standard_user")
driver.find_element(By.ID, "password").send_keys("secret_sauce")
driver.find_element(By.ID, "login-button").click()
assert "inventory.html" in driver.current_url
def test_invalid_password(driver):
driver.find_element(By.ID, "user-name").send_keys("standard_user") # дубль
driver.find_element(By.ID, "password").send_keys("wrong_password") # дубль
driver.find_element(By.ID, "login-button").click() # дубль
error = driver.find_element(By.CLASS_NAME, "error-message-container")
assert "do not match" in error.text
- Дублирование кода — одинаковые строки поиска элементов во всех тестах
- Сложность поддержки — если
id="user-name"изменится, нужно обновить каждый тест вручную - Сложность масштабирования — чем больше тестов, тем хаотичнее разрозненные локаторы
- Увеличение времени написания — каждый новый тест требует ручного поиска нужных локаторов
Что такое Page Object Model
Из лекции: Page Object Model (POM) — паттерн проектирования для автоматизированного тестирования UI. Каждая страница приложения описывается отдельным классом, который инкапсулирует все локаторы и методы работы с элементами этой страницы.
Основные принципы POM (из лекции)
- Отделение логики тестов от локаторов — тест не знает, как найти элемент, только что с ним делать
- Локаторы хранятся в классах страниц — один класс = одна страница = одно место для всех локаторов
- Централизованное управление — если изменился селектор, правим только в Page Object, тесты не трогаем
- Повторное использование — один
LoginPageиспользуется во всех тестах, где нужен логин - Читаемость тестов —
login_page.enter_username("user")понятнее, чемdriver.find_element(By.ID, "user-name").send_keys("user")
Структура POM-проекта
Из лекции: типичная структура проекта с POM:
saucedemo_tests/
├── conftest.py # глобальные фикстуры (driver)
├── pages/
│ ├── __init__.py # пустой — делает pages/ пакетом Python
│ ├── login_page.py # страница логина
│ ├── inventory_page.py # страница инвентаря
│ ├── cart_page.py # страница корзины
│ └── checkout_page.py # страница оформления заказа
└── tests/
├── __init__.py # пустой — делает tests/ пакетом Python
├── base_test.py # базовый тестовый класс (инициализация)
├── test_login.py # тесты логина
├── test_inventory.py # тесты страницы инвентаря
└── test_cart.py # тесты корзины
__init__.py? Этот пустой файл сигнализирует Python, что директория является пакетом. Без него импорт from pages.login_page import LoginPage завершится с ModuleNotFoundError.
BasePage — базовый класс страницы
В больших проектах создают общий BasePage с повторяющимися методами: ожидание элемента, клик, ввод текста. Конкретные страницы наследуются от него.
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
"""Базовый класс для всех Page Objects. Содержит общие методы."""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def find(self, locator):
"""Ожидает появления элемента и возвращает его."""
return self.wait.until(EC.presence_of_element_located(locator))
def click(self, locator):
"""Ожидает кликабельности и выполняет клик."""
self.wait.until(EC.element_to_be_clickable(locator)).click()
def type_text(self, locator, text):
"""Очищает поле ввода и вводит текст."""
field = self.find(locator)
field.clear()
field.send_keys(text)
def get_text(self, locator):
"""Возвращает текстовое содержимое элемента."""
return self.find(locator).text
def find_all(self, locator):
"""Возвращает все элементы по локатору."""
return self.wait.until(EC.presence_of_all_elements_located(locator))
def is_visible(self, locator):
"""Проверяет, что элемент видим."""
return self.wait.until(EC.visibility_of_element_located(locator)).is_displayed()
LoginPage — пример класса страницы
# pages/login_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class LoginPage(BasePage):
# Локаторы — атрибуты класса (не внутри методов!)
USERNAME_INPUT = (By.ID, "user-name")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-button")
ERROR_MESSAGE = (By.CLASS_NAME, "error-message-container")
URL = "https://www.saucedemo.com/"
def open(self):
self.driver.get(self.URL)
def enter_username(self, username):
self.type_text(self.USERNAME_INPUT, username)
def enter_password(self, password):
self.type_text(self.PASSWORD_INPUT, password)
def click_login(self):
self.click(self.LOGIN_BUTTON)
def success_login(self, username, password):
"""Выполняет полный цикл авторизации."""
self.enter_username(username)
self.enter_password(password)
self.click_login()
def get_error_message(self):
return self.get_text(self.ERROR_MESSAGE)
LOCATOR = (By.X, "value") на уровне класса (а не внутри методов) — лучшая практика. Они видны сразу при чтении класса и удобны для переопределения в подклассах.
pytest-фикстуры и scope="class"
Из лекции: scope="class" означает, что один и тот же браузер используется во всех тестах внутри класса. Это ускоряет тесты — браузер создаётся не перед каждым тестом, а один раз на весь класс.
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
@pytest.fixture(scope="class")
def driver():
"""Фикстура уровня класса: один браузер на весь класс тестов."""
d = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))
d.maximize_window()
d.get("https://www.saucedemo.com/")
yield d
d.quit()
Когда scope="class" хорошо и когда плохо
| Хорошо, когда | Плохо, когда |
|---|---|
| Тесты не зависят друг от друга и могут использовать один браузер | Один тест оставляет браузер в некорректном состоянии (переходит на другую страницу, добавляет товары) |
| Нужно ускорить выполнение за счёт повторного использования driver | Нужно, чтобы каждый тест начинался с «чистого» состояния |
Базовый тестовый класс: BaseTest
Из лекции: паттерн — вынести инициализацию driver и Page Objects в базовый тестовый класс с autouse=True:
# tests/base_test.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from pages.login_page import LoginPage
from pages.inventory_page import InventoryPage
from pages.cart_page import CartPage
class BaseTest:
@pytest.fixture(scope="class", autouse=True)
def setup(self):
# Инициализация браузера
self.driver = webdriver.Chrome(
service=ChromeService(ChromeDriverManager().install())
)
self.driver.maximize_window()
self.driver.get("https://www.saucedemo.com/")
# Инициализация Page Objects
self.login_page = LoginPage(self.driver)
self.inventory_page = InventoryPage(self.driver)
self.cart_page = CartPage(self.driver)
yield # тесты выполняются здесь
self.driver.quit() # teardown — закрываем браузер
Тестовые классы наследуются от BaseTest и автоматически получают доступ ко всем атрибутам:
# tests/test_inventory.py
from tests.base_test import BaseTest
class TestInventory(BaseTest):
def test_items_amount(self):
# self.login_page, self.inventory_page — уже доступны через BaseTest
self.login_page.success_login("standard_user", "secret_sauce")
assert self.inventory_page.get_items_amount() == 6
- Избавляет от дублирования — driver и страницы создаются один раз
- Тесты чище — только логика проверок, без инициализации
- Гибкость — для смены браузера достаточно изменить только
BaseTest
Итог: блок Selenium (уроки 03–12)
Блок Selenium завершён. Вот краткая схема пройденного пути:
| Урок | Тема | Ключевые концепции |
|---|---|---|
| 03 | Введение в Selenium | webdriver, find_element, By, click, send_keys |
| 04 | Summary 2 | Повторение основ Selenium |
| 05 | Локаторы | By.ID/NAME/CLASS/CSS/XPATH, стратегии поиска |
| 06 | Summary 3 | Повторение локаторов |
| 07 | Расширенные практики (1) | implicit/explicit wait, WebDriverWait, EC |
| 08 | Summary 4 | Повторение ожиданий |
| 09 | Расширенные практики (2) | Alert, switch_to, ActionChains, iframe, file upload |
| 10 | Summary 5 | Повторение Alert/ActionChains/iframe |
| 11 | Page Object Model | POM, BasePage, BaseTest, scope="class" |
| 12 | Summary 6 | Повторение POM (этот урок) |