💻 Примеры: POM на практике

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

Показательные мини-примеры Page Object Model для saucedemo.com.

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

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

Пример 1: LoginPage.success_login()

Метод success_login() объединяет три шага — ввод логина, пароля и клик. Тест становится однострочным.

# 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)
# tests/test_login.py
from tests.base_test import BaseTest


class TestLogin(BaseTest):
    def test_valid_login(self):
        # Один вызов вместо трёх find_element
        self.login_page.success_login("standard_user", "secret_sauce")
        assert "inventory.html" in self.driver.current_url

    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

Пример 2: TestInventory — проверки страницы инвентаря

Из лекции: TestInventory использует фикстуры scope="class" для одного браузера на весь класс.

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


class InventoryPage(BasePage):
    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_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):
        """Цена товара по названию (через XPath)."""
        xpath = (
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='inventory_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.find((By.XPATH, xpath)).text

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

    def go_to_cart(self):
        self.click(self.CART_LINK)
# tests/test_inventory.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 TestInventory:
    @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 login_page(self, driver):
        return LoginPage(driver)

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

    def test_items_amount(self, login_page, inventory_page):
        login_page.success_login("standard_user", "secret_sauce")
        assert inventory_page.get_items_amount() == 6

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

    def test_all_names_contain_sauce_labs(self, login_page, inventory_page):
        login_page.success_login("standard_user", "secret_sauce")
        assert inventory_page.all_item_names_contains_sauce_labs()

Пример 3: TestCart — цена в корзине совпадает с инвентарём

Из лекции: тест сравнивает цену товара на странице инвентаря с ценой в корзине.

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


class CartPage(BasePage):
    CHECKOUT_BUTTON = (By.ID, "checkout")

    def get_cart_item_price(self, item_name):
        """Цена товара в корзине по названию (через XPath)."""
        xpath = (
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='cart_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.find((By.XPATH, xpath)).text

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

    def proceed_to_checkout(self):
        self.click(self.CHECKOUT_BUTTON)
# tests/test_cart.py
from tests.base_test import BaseTest


class TestCart(BaseTest):
    def test_backpack_price_in_cart(self):
        # 1. Логин
        self.login_page.success_login("standard_user", "secret_sauce")
        # 2. Запоминаем цену на странице инвентаря
        backpack_price = self.inventory_page.get_item_price("Sauce Labs Backpack")
        # 3. Добавляем в корзину и переходим
        self.inventory_page.add_item_to_cart("Sauce Labs Backpack")
        self.inventory_page.go_to_cart()
        # 4. Проверяем: цена в корзине = цена на инвентаре
        cart_price = self.cart_page.get_cart_item_price("Sauce Labs Backpack")
        assert backpack_price == cart_price, (
            f"Цена не совпадает: инвентарь={backpack_price}, корзина={cart_price}"
        )

Пример 4: BaseTest — единая точка инициализации

Из лекции: BaseTest с 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/")

        self.login_page     = LoginPage(self.driver)
        self.inventory_page = InventoryPage(self.driver)
        self.cart_page      = CartPage(self.driver)

        yield

        self.driver.quit()
Как это работает: любой класс, унаследованный от BaseTest, получает self.driver, self.login_page, self.inventory_page, self.cart_page автоматически — без повторного написания setup-кода.