🏠 Домашнее задание 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

Полный текст ДЗ 6 из LMS (Auto QA: Домашнее задание 6).

Напишите автоматизированный тест с использованием Page Object Model (POM), который выполняет следующие шаги:

  1. Откройте сайт магазина: https://www.saucedemo.com/
  2. Авторизуйтесь как пользователь standard_user
  3. Добавьте в корзину товары:
    • Sauce Labs Backpack
    • Sauce Labs Bolt T-Shirt
    • Sauce Labs Onesie
  4. Перейдите в корзину
  5. Нажмите Checkout
  6. Заполните форму своими данными:
    • Имя
    • Фамилия
    • Почтовый индекс
  7. Прочтите со страницы итоговую стоимость (Total)
  8. Закройте браузер
  9. Проверьте, что итоговая сумма равна $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"

Пошаговое решение

Решение строится снизу вверх: сначала вспомогательные классы (BasePage), потом классы страниц, потом тест.

Файл 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 запустит тест в отладочном режиме.

Точки останова

Чтобы отладить тест пошагово:

  1. Откройте tests/test_checkout.py в VS Code
  2. Кликните слева от строки self.inventory_page.add_item_to_cart("Sauce Labs Backpack") — появится красная точка
  3. Нажмите F5 — выполнение остановится на этой строке
  4. В панели Variables (слева) видны self.driver, self.login_page и их атрибуты
  5. F10 — следующая строка, F11 — зайти внутрь метода (например, в add_item_to_cart)
Совет: поставьте точку останова перед fill_checkout_form() — в это время браузер открыт на странице корзины после клика Checkout. Вы видите реальное состояние DOM прямо в момент выполнения теста.