⚖️ Старый vs Новый: рефакторинг в POM

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

Примеры из лекции (старый стиль) → рефакторинг в POM с современными практиками.

⚡ Ключевые изменения

  • Локаторы в тестах → локаторы как атрибуты класса страницы
  • driver.find_element() в тесте → метод страницы
  • Дублирование кода → один метод success_login()
  • Поиск через find_element без ожидания → WebDriverWait + EC в BasePage
  • Без BasePageBasePage с общими методами

Сравнение 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 — наследуйся и используй