Структура файлов 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 строки поиска и взаимодействия в каждом тесте |