🏠 Домашнее задание 6: Page Object Model
⚡ ДЗ 6 кратко
Автотест POM: логин → 3 товара в корзину → checkout → проверка суммы $58.29.
- Файлы:
pages/login_page.py,pages/inventory_page.py,pages/cart_page.py,pages/checkout_page.py,tests/test_checkout.py - Локаторы только в классах страниц, тест только вызывает методы
- Запуск:
python -m pytest tests/test_checkout.py -v
Задание из LMS
Напишите автоматизированный тест с использованием Page Object Model (POM), который выполняет следующие шаги:
- Откройте сайт магазина: https://www.saucedemo.com/
- Авторизуйтесь как пользователь
standard_user - Добавьте в корзину товары:
- Sauce Labs Backpack
- Sauce Labs Bolt T-Shirt
- Sauce Labs Onesie
- Перейдите в корзину
- Нажмите Checkout
- Заполните форму своими данными:
- Имя
- Фамилия
- Почтовый индекс
- Прочтите со страницы итоговую стоимость (Total)
- Закройте браузер
- Проверьте, что итоговая сумма равна $58.29
Требования:
- Использовать Page Object Model для организации кода
- Вынести все локаторы и методы работы со страницами в отдельные классы (Page Object)
- Тест должен быть независимым и запускаться без предварительной подготовки данных
Подготовка окружения
Шаг 1: Создание виртуального окружения
# PowerShell — в корне проекта
python -m venv .venv
.\.venv\Scripts\Activate.ps1
Шаг 2: Установка зависимостей
pip install selenium pytest webdriver-manager
Шаг 3: Структура проекта
# Создаём директории и файлы
mkdir saucedemo_tests
cd saucedemo_tests
mkdir pages tests
# Создаём __init__.py
New-Item -Path "pages\__init__.py" -ItemType File
New-Item -Path "tests\__init__.py" -ItemType File
New-Item -Path "conftest.py" -ItemType File
Итоговая структура:
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_checkout.py
Шаг 4: Инициализация git
git init
git add .
git commit -m "initial: POM project structure"
Пошаговое решение
Файл 1: pages/base_page.py — базовый класс
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
"""Базовый класс. Содержит общие методы для всех Page Objects."""
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))
Зачем: WebDriverWait создаётся один раз в базовом классе. Все остальные Page Objects наследуют эти методы и не дублируют код ожидания.
Файл 2: pages/login_page.py — страница логина
# 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")
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()
Связь с теорией: метод success_login() инкапсулирует 3 действия. Тест вызывает один метод вместо трёх строк. Подробнее — теория POM.
Файл 3: pages/inventory_page.py — страница инвентаря
# pages/inventory_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class InventoryPage(BasePage):
CART_LINK = (By.CLASS_NAME, "shopping_cart_link")
def get_item_price(self, item_name):
locator = (
By.XPATH,
f"//div[text()='{item_name}']"
f"/ancestor::div[@class='inventory_item']"
f"//div[@class='inventory_item_price']"
)
return self.get_text(locator)
def add_item_to_cart(self, item_name):
locator = (
By.XPATH,
f"//div[text()='{item_name}']"
f"/ancestor::div[@class='inventory_item']//button"
)
self.click(locator)
def go_to_cart(self):
self.click(self.CART_LINK)
Пояснение XPath: //div[text()='...'] находит div с точным текстом, /ancestor::div[@class='inventory_item'] поднимается к родительскому контейнеру товара, //div[@class='inventory_item_price'] находит цену внутри этого контейнера. Подробнее — примеры.
Файл 4: pages/cart_page.py — страница корзины
# 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 proceed_to_checkout(self):
self.click(self.CHECKOUT_BUTTON)
Файл 5: pages/checkout_page.py — страница оформления
# 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):
"""Заполняет форму и нажимает Continue."""
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)
Файл 6: tests/base_test.py — базовый тестовый класс
# 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()
Файл 7: tests/test_checkout.py — тест ДЗ
# tests/test_checkout.py
from tests.base_test import BaseTest
class TestCheckout(BaseTest):
def test_checkout_total_price(self):
# 1. Открываем сайт и авторизуемся
self.login_page.open()
self.login_page.success_login("standard_user", "secret_sauce")
# 2. Добавляем три товара в корзину
self.inventory_page.add_item_to_cart("Sauce Labs Backpack")
self.inventory_page.add_item_to_cart("Sauce Labs Bolt T-Shirt")
self.inventory_page.add_item_to_cart("Sauce Labs Onesie")
# 3. Переходим в корзину
self.inventory_page.go_to_cart()
# 4. Нажимаем Checkout
self.cart_page.proceed_to_checkout()
# 5. Заполняем форму (данные произвольные)
self.checkout_page.fill_checkout_form("John", "Doe", "12345")
# 6. Читаем итоговую сумму
total_text = self.checkout_page.get_total_price()
print(f"Итоговая сумма: {total_text}")
# 7. Проверяем, что сумма равна $58.29
assert "$58.29" in total_text, \
f"Ожидалась сумма $58.29, получено: {total_text}"
"$58.29" in total_text, а не == "$58.29"?Страница saucedemo.com отображает строку вида
"Total: $58.29". Поэтому проверяем вхождение суммы в строку, а не равенство целой строки. Можно также использовать total_text.split(": ")[1], чтобы получить только число.
Проверка в VS Code
Запуск через терминал
# Активировать venv (если не активировано)
.\.venv\Scripts\Activate.ps1
# Запустить тест с подробным выводом
python -m pytest tests/test_checkout.py -v
# Запустить с выводом print()
python -m pytest tests/test_checkout.py -v -s
Ожидаемый вывод:
tests/test_checkout.py::TestCheckout::test_checkout_total_price PASSED [100%]
Итоговая сумма: Total: $58.29
1 passed in 15.34s
Запуск через F5 (launch.json)
Создайте файл .vscode/launch.json:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "pytest: test_checkout",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["tests/test_checkout.py", "-v", "-s"],
"cwd": "${workspaceFolder}",
"env": {}
}
]
}
После создания: нажмите F5 (или Run → Start Debugging) → VS Code запустит тест в отладочном режиме.
Точки останова
Чтобы отладить тест пошагово:
- Откройте
tests/test_checkout.pyв VS Code - Кликните слева от строки
self.inventory_page.add_item_to_cart("Sauce Labs Backpack")— появится красная точка - Нажмите F5 — выполнение остановится на этой строке
- В панели Variables (слева) видны
self.driver,self.login_pageи их атрибуты - F10 — следующая строка, F11 — зайти внутрь метода (например, в
add_item_to_cart)
fill_checkout_form() — в это время браузер открыт на странице корзины после клика Checkout. Вы видите реальное состояние DOM прямо в момент выполнения теста.
Связь с разделами урока
- Теория — принципы POM, BasePage, BaseTest, scope фикстур
- Примеры — полная структура проекта, все 5 классов страниц
- Справочник — шаблоны всех классов для копирования
- Ошибки — ModuleNotFoundError, scope="class" с зависимыми тестами