⚖️ Старый vs Новый: рефакторинг в POM
Примеры из лекции (старый стиль) → рефакторинг в POM с современными практиками.
⚡ Ключевые изменения
- Локаторы в тестах → локаторы как атрибуты класса страницы
driver.find_element()в тесте → метод страницы- Дублирование кода → один метод
success_login() - Поиск через
find_elementбез ожидания →WebDriverWait + ECв BasePage - Без
BasePage→BasePageс общими методами
Сравнение 1: Тест логина — до и после
❌ Старый стиль (из лекции, не использовать)
# tests/test_login_old.py
# Локаторы прямо в тестах — плохо!
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service as ChromeService
@pytest.fixture
def driver():
driver = webdriver.Chrome(
service=ChromeService(ChromeDriverManager().install())
)
driver.maximize_window()
driver.get("https://www.saucedemo.com/")
yield driver
driver.quit()
def test_success_login_valid_data(driver):
# Локаторы прямо здесь — дублируются в каждом тесте!
user_name_input = driver.find_element(By.ID, "user-name")
user_name_input.send_keys("standard_user")
password_input = driver.find_element(By.ID, "password")
password_input.send_keys("secret_sauce")
login_button = driver.find_element(By.ID, "login-button")
login_button.click()
assert driver.current_url == \
"https://www.saucedemo.com/inventory.html"
def test_invalid_password(driver):
# Те же 3 локатора снова — нарушение DRY!
user_name_input = driver.find_element(By.ID, "user-name")
user_name_input.send_keys("standard_user")
password_input = driver.find_element(By.ID, "password")
password_input.send_keys("wrong_password")
login_button = driver.find_element(By.ID, "login-button")
login_button.click()
error = driver.find_element(By.CLASS_NAME, "error-message-container")
assert "Username and password do not match" in error.text
✅ POM-стиль (современный, Selenium 4)
# 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 success_login(self, username, password):
self.type_text(self.USERNAME_INPUT, username)
self.type_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
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_success_login_valid_data(self):
self.login_page.open()
# Один вызов вместо 6 строк!
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: LoginPage из лекции → с BasePage
❌ LoginPage из лекции (без BasePage)
# pages/login_page.py — версия из лекции
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
def __init__(self, driver):
self.driver = driver
# WebDriverWait повторяется в каждом Page Object!
self.wait = WebDriverWait(driver, 10)
def open(self):
self.driver.get("https://www.saucedemo.com/")
def get_username_input(self):
return self.wait.until(
EC.presence_of_element_located((By.ID, "user-name"))
)
def get_password_input(self):
return self.wait.until(
EC.presence_of_element_located((By.ID, "password"))
)
def get_login_button(self):
return self.wait.until(
EC.element_to_be_clickable((By.ID, "login-button"))
)
def enter_username(self, username):
field = self.get_username_input()
field.clear()
field.send_keys(username)
def enter_password(self, password):
field = self.get_password_input()
field.clear()
field.send_keys(password)
def click_on_login_button(self):
self.get_login_button().click()
✅ LoginPage с 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):
el = self.find(locator)
el.clear()
el.send_keys(text)
def get_text(self, locator):
return self.find(locator).text
# pages/login_page.py — только специфика LoginPage
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 success_login(self, username, password):
self.type_text(self.USERNAME_INPUT, username)
self.type_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
def get_error_message(self):
return self.get_text(self.ERROR_MESSAGE)
Сравнение 3: Тест корзины — до и после
❌ Без POM — хаотично
# Всё в одном тесте — нечитаемо
def test_backpack_price(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()
# цена backpack
price_el = driver.find_element(
By.XPATH,
"//div[text()='Sauce Labs Backpack']"
"/ancestor::div[@class='inventory_item']"
"//div[@class='inventory_item_price']"
)
inventory_price = price_el.text
# добавить в корзину
btn = driver.find_element(
By.XPATH,
"//div[text()='Sauce Labs Backpack']"
"/ancestor::div[@class='inventory_item']//button"
)
btn.click()
# перейти в корзину
driver.find_element(By.CLASS_NAME, "shopping_cart_link").click()
# цена в корзине
cart_price_el = driver.find_element(
By.XPATH,
"//div[text()='Sauce Labs Backpack']"
"/ancestor::div[@class='cart_item']"
"//div[@class='inventory_item_price']"
)
assert inventory_price == cart_price_el.text
✅ С POM — читаемо и поддерживаемо
# tests/test_cart.py
from tests.base_test import BaseTest
class TestCart(BaseTest):
def test_backpack_price(self):
# Каждая строка читается как текст!
self.login_page.open()
self.login_page.success_login(
"standard_user", "secret_sauce"
)
inventory_price = self.inventory_page.get_item_price(
"Sauce Labs Backpack"
)
self.inventory_page.add_item_to_cart("Sauce Labs Backpack")
self.inventory_page.go_to_cart()
cart_price = self.cart_page.get_cart_item_price(
"Sauce Labs Backpack"
)
assert inventory_price == cart_price, \
"Цена в корзине не совпадает с ценой в инвентаре"
Итоги: что менять при рефакторинге в POM
| Было (из лекции) | Стало (современный POM) |
|---|---|
driver.find_element(By.ID, "user-name") в тесте |
LoginPage.USERNAME_INPUT = (By.ID, "user-name") |
| WebDriverWait в каждом Page Object заново | WebDriverWait один раз в BasePage |
| get_username_input() возвращает элемент | enter_username() выполняет действие целиком |
| click_on_login_button() — многословно | click_login() — кратко; success_login() — всё сразу |
| Фикстуры в каждом тестовом классе заново | BaseTest с autouse=True — наследуйся и используй |