💻 Примеры: Page Object Model

← К оглавлению урока

Полная POM-структура проекта для saucedemo.com — от base_page до готовых тестов.

⚡ Структура проекта

pages/base_page.py   → BasePage (find, click, type_text)
pages/login_page.py  → LoginPage(BasePage) + success_login()
pages/inventory_page.py → InventoryPage + add_item_to_cart()
pages/cart_page.py   → CartPage + get_cart_item_price()
tests/base_test.py   → BaseTest (autouse setup + driver + pages)
tests/test_login.py  → TestLogin(BaseTest)
tests/test_inventory.py → TestInventory(BaseTest)

Пример 1: Полная структура POM-проекта

Все файлы ниже — части одного проекта. Создайте структуру директорий, скопируйте каждый блок в соответствующий файл.

Шаг 1: Базовый класс страницы

# 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):
        """Возвращает True, если элемент видим."""
        return self.wait.until(
            EC.visibility_of_element_located(locator)
        ).is_displayed()

Шаг 2: LoginPage — страница логина

# pages/login_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class LoginPage(BasePage):
    """Page Object для страницы авторизации saucedemo.com."""

    # Локаторы — атрибуты класса
    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)

Шаг 3: InventoryPage — страница инвентаря

# pages/inventory_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class InventoryPage(BasePage):
    """Page Object для страницы каталога товаров."""

    INVENTORY_ITEMS = (By.CLASS_NAME, "inventory_item")
    ITEM_NAMES      = (By.CLASS_NAME, "inventory_item_name")
    CART_LINK       = (By.CLASS_NAME, "shopping_cart_link")

    def get_items(self):
        return self.find_all(self.INVENTORY_ITEMS)

    def get_items_amount(self):
        return len(self.get_items())

    def all_items_are_displayed(self):
        return all(item.is_displayed() for item in self.get_items())

    def get_item_names(self):
        return [el.text for el in self.find_all(self.ITEM_NAMES)]

    def all_items_names_are_displayed(self):
        return all(name.strip() != "" for name in self.get_item_names())

    def all_item_names_are_not_empty(self):
        return all(bool(name.strip()) for name in self.get_item_names())

    def all_item_names_contains_sauce_labs(self):
        return all(name.startswith("Sauce Labs") for name in self.get_item_names())

    def get_item_price(self, item_name):
        """Возвращает цену товара по его названию."""
        locator = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='inventory_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.get_text(locator)

    def add_item_to_cart(self, item_name):
        """Добавляет товар в корзину по названию."""
        locator = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='inventory_item']//button"
        )
        self.click(locator)

    def go_to_cart(self):
        """Кликает на иконку корзины."""
        self.click(self.CART_LINK)

Шаг 4: CartPage — страница корзины

# pages/cart_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class CartPage(BasePage):
    """Page Object для страницы корзины."""

    CHECKOUT_BUTTON  = (By.ID, "checkout")
    ITEM_PRICES      = (By.CLASS_NAME, "inventory_item_price")

    def get_cart_item_price(self, item_name):
        """Возвращает цену конкретного товара в корзине."""
        locator = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='cart_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.get_text(locator)

    def get_items_prices(self):
        """Возвращает список цен всех товаров в корзине."""
        return [el.text for el in self.find_all(self.ITEM_PRICES)]

    def remove_item_from_cart(self, item_name):
        locator = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='cart_item']//button"
        )
        self.click(locator)

    def proceed_to_checkout(self):
        self.click(self.CHECKOUT_BUTTON)

Шаг 5: CheckoutPage — страница оформления заказа

# pages/checkout_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class CheckoutPage(BasePage):
    """Page Object для страницы оформления заказа."""

    FIRST_NAME_INPUT = (By.ID, "first-name")
    LAST_NAME_INPUT  = (By.ID, "last-name")
    ZIP_CODE_INPUT   = (By.ID, "postal-code")
    CONTINUE_BUTTON  = (By.ID, "continue")
    TOTAL_LABEL      = (By.CLASS_NAME, "summary_total_label")

    def fill_checkout_form(self, first_name, last_name, zip_code):
        """Заполняет форму оформления заказа и нажимает Continue."""
        self.type_text(self.FIRST_NAME_INPUT, first_name)
        self.type_text(self.LAST_NAME_INPUT, last_name)
        self.type_text(self.ZIP_CODE_INPUT, zip_code)
        self.click(self.CONTINUE_BUTTON)

    def get_total_price(self):
        """Возвращает строку с итоговой суммой."""
        return self.get_text(self.TOTAL_LABEL)

Шаг 6: BaseTest — базовый тестовый класс

# 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
from pages.checkout_page import CheckoutPage


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)
        self.checkout_page   = CheckoutPage(self.driver)

        yield  # здесь выполняются тесты

        self.driver.quit()  # закрываем браузер после всех тестов класса

Шаг 7: Тесты логина

# tests/test_login.py
from tests.base_test import BaseTest


class TestLogin(BaseTest):

    def test_successful_login(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert "inventory.html" in self.driver.current_url, \
            "После входа URL должен содержать 'inventory.html'"

    def test_invalid_password(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "wrong_password")
        error = self.login_page.get_error_message()
        assert "Username and password do not match" in error

    def test_locked_out_user(self):
        self.login_page.open()
        self.login_page.success_login("locked_out_user", "secret_sauce")
        error = self.login_page.get_error_message()
        assert "Sorry, this user has been locked out." in error

    def test_empty_username(self):
        self.login_page.open()
        self.login_page.enter_password("secret_sauce")
        self.login_page.click_login()
        error = self.login_page.get_error_message()
        assert "Username is required" in error

    def test_empty_password(self):
        self.login_page.open()
        self.login_page.enter_username("standard_user")
        self.login_page.click_login()
        error = self.login_page.get_error_message()
        assert "Password is required" in error

Шаг 8: Тесты инвентаря

# tests/test_inventory.py
from tests.base_test import BaseTest


class TestInventory(BaseTest):

    def test_items_amount(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.get_items_amount() == 6, \
            "Количество товаров должно быть 6"

    def test_all_items_are_displayed(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_items_are_displayed()

    def test_all_items_names_are_displayed(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_items_names_are_displayed()

    def test_all_item_names_are_not_empty(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_item_names_are_not_empty()

    def test_all_item_names_contains_sauce_labs(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_item_names_contains_sauce_labs(), \
            "Все названия товаров должны начинаться с 'Sauce Labs'"

Шаг 9: Тест корзины (из лекции)

# tests/test_cart.py
from tests.base_test import BaseTest


class TestCart(BaseTest):

    def test_backpack_price_matches_cart(self):
        """Цена товара на странице инвентаря должна совпадать с ценой в корзине."""
        # 1. Авторизация
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")

        # 2. Запоминаем цену Backpack на странице инвентаря
        backpack_price = self.inventory_page.get_item_price("Sauce Labs Backpack")

        # 3. Добавляем в корзину
        self.inventory_page.add_item_to_cart("Sauce Labs Backpack")

        # 4. Переходим в корзину
        self.inventory_page.go_to_cart()

        # 5. Сравниваем цены
        cart_price = self.cart_page.get_cart_item_price("Sauce Labs Backpack")
        assert backpack_price == cart_price, \
            f"Цена в корзине ({cart_price}) не совпадает с ценой в инвентаре ({backpack_price})"

Пример 2: Тесты с фикстурами scope="class" (из лекции)

Альтернативный подход из лекции — фикстуры внутри тестового класса без наследования:

# tests/test_inventory_fixtures.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from pages.inventory_page import InventoryPage
from pages.login_page import LoginPage


class TestInventoryFixtures:

    @pytest.fixture(scope="class")
    def driver(self):
        d = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))
        d.maximize_window()
        d.get("https://www.saucedemo.com/")
        yield d
        d.quit()

    @pytest.fixture(scope="class")
    def inventory_page(self, driver):
        return InventoryPage(driver)

    @pytest.fixture(scope="class")
    def login_page(self, driver):
        return LoginPage(driver)

    def test_items_amount(self, inventory_page, login_page):
        # pytest автоматически подставляет фикстуры как аргументы метода
        login_page.success_login("standard_user", "secret_sauce")
        assert inventory_page.get_items_amount() == 6
Разница двух подходов:
  • BaseTest (наследование): страницы доступны через self.login_page — удобно, когда много тестовых классов
  • Фикстуры в классе: страницы передаются как аргументы методов — более явно, лучше изолировано
На практике предпочтительнее конфигурация через conftest.py с фикстурами — они работают без наследования и автоматически доступны в любом тесте проекта.