🔖 Справочник: Page Object Model

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

⚡ Шпаргалка POM

  • pages/__init__.py — пустой, делает папку пакетом
  • BasePage.__init__(driver)self.driver, self.wait = WebDriverWait(driver, 10)
  • Локатор как атрибут: LOCATOR = (By.ID, "value")
  • @pytest.fixture(scope="class") — один driver на класс
  • @pytest.fixture(scope="class", autouse=True) — автоматическая инициализация
  • Импорт: from pages.login_page import LoginPage

Структура файлов POM-проекта

saucedemo_tests/
├── conftest.py
├── pages/
│   ├── __init__.py
│   ├── base_page.py
│   ├── login_page.py
│   ├── inventory_page.py
│   ├── cart_page.py
│   └── checkout_page.py
└── tests/
    ├── __init__.py
    ├── base_test.py
    ├── test_login.py
    ├── test_inventory.py
    └── test_cart.py

Шаблон: BasePage

# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class BasePage:
    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))

Шаблон: 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)

Шаблон: InventoryPage

# 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_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):
        """Возвращает строку цены (напр. '$29.99') для товара по названию."""
        xpath = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='inventory_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.get_text(xpath)

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

    def go_to_cart(self):
        self.click(self.CART_LINK)

Шаблон: CartPage

# 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 = (
            By.XPATH,
            f"//div[text()='{item_name}']"
            f"/ancestor::div[@class='cart_item']"
            f"//div[@class='inventory_item_price']"
        )
        return self.get_text(xpath)

    def get_items_prices(self):
        prices = self.find_all((By.CLASS_NAME, "inventory_item_price"))
        return [p.text for p in prices]

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

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

Шаблон: CheckoutPage

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


class CheckoutPage(BasePage):
    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):
        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):
        """Возвращает итоговую сумму (напр. 'Total: $58.29')."""
        return self.get_text(self.TOTAL_LABEL)

Шаблон: conftest.py

# 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="function")
def driver():
    d = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))
    d.maximize_window()
    yield d
    d.quit()

Шаблон: 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/")

        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()

Шаблон тестового класса с наследованием

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


class TestInventory(BaseTest):

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

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

Запуск тестов

# Запуск всех тестов
python -m pytest tests/ -v

# Запуск конкретного файла
python -m pytest tests/test_login.py -v

# Запуск конкретного теста
python -m pytest tests/test_login.py::TestLogin::test_valid_login -v

# Запуск с выводом print()
python -m pytest tests/ -v -s

Ключевые принципы оформления локаторов

ХорошоПлохо
USERNAME_INPUT = (By.ID, "user-name") — атрибут класса driver.find_element(By.ID, "user-name") — прямо в тесте
Локатор в одном месте — меняем один раз Локатор в 10 тестах — меняем 10 раз
login_page.success_login("user", "pass") — читаемый метод 3 строки поиска и взаимодействия в каждом тесте