📖 Теория: 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
Преимущества BaseTest (из лекции):
  • Избавляет от дублирования — driver и страницы создаются один раз
  • Тесты чище — только логика проверок, без инициализации
  • Гибкость — для смены браузера достаточно изменить только BaseTest

Итог: блок Selenium (уроки 03–12)

Блок Selenium завершён. Вот краткая схема пройденного пути:

УрокТемаКлючевые концепции
03Введение в Seleniumwebdriver, find_element, By, click, send_keys
04Summary 2Повторение основ Selenium
05ЛокаторыBy.ID/NAME/CLASS/CSS/XPATH, стратегии поиска
06Summary 3Повторение локаторов
07Расширенные практики (1)implicit/explicit wait, WebDriverWait, EC
08Summary 4Повторение ожиданий
09Расширенные практики (2)Alert, switch_to, ActionChains, iframe, file upload
10Summary 5Повторение Alert/ActionChains/iframe
11Page Object ModelPOM, BasePage, BaseTest, scope="class"
12Summary 6Повторение POM (этот урок)