Python: Selenium

Теория: Структуризация тестов

Когда автотестов становится много, важно держать порядок. Отдельные файлы и папки помогают быстро найти нужный тест, локатор или фикстуру. Pytest сам найдёт тесты, если они начинаются с test_ и лежат в папке tests.

Создание проекта

## создаём папку проекта
mkdir selenium-tests
cd selenium-tests

## заводим окружение и добавляем зависимости через uv
uv venv --python 3.14 .venv
uv add selenium pytest allure-pytest

## создаём базовую структуру
mkdir tests pages core data utils reports
touch pytest.ini tests/conftest.py

pytest.ini задаёт общие параметры запуска:

[pytest]
addopts = -vv --tb=auto
testpaths = tests
markers =
    smoke: быстрые проверки
    regression: полный набор тестов

Папка tests/ — сценарии

Здесь лежат сами тесты. Каждый модуль — отдельная часть функциональности.

tests/
├── auth/
│   ├── test_login_positive.py
│   └── test_login_negative.py
├── profile/
│   └── test_profile_edit.py
└── conftest.py

В conftest.py хранятся фикстуры — общий код для всех тестов.

## tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture
def driver():
    opts = Options()
    opts.add_argument("--window-size=1366,768")
    opts.add_argument("--headless=new")
    driver = webdriver.Chrome(options=opts)
    yield driver
    driver.quit()

Так браузер создаётся и закрывается автоматически, без повторений в каждом тесте.

Папка pages/ — страницы

Страницы описывают элементы и действия. Это шаблон Page Object: тест обращается к методам страницы, а не к локаторам напрямую.

## pages/login_page.py
from selenium.webdriver.common.by import By

class LoginPage:
    URL = "https://the-internet.herokuapp.com/login"
    USER = (By.ID, "username")
    PASS = (By.ID, "password")
    BTN = (By.CSS_SELECTOR, "button[type='submit']")
    FLASH = (By.ID, "flash")

    def __init__(self, driver):
        self.driver = driver

    def open(self):
        self.driver.get(self.URL)

    def login(self, username, password):
        self.driver.find_element(*self.USER).send_keys(username)
        self.driver.find_element(*self.PASS).send_keys(password)
        self.driver.find_element(*self.BTN).click()

    def message(self):
        return self.driver.find_element(*self.FLASH).text

Пример теста:

## tests/auth/test_login_positive.py
from pages.login_page import LoginPage

def test_login_success(driver):
    page = LoginPage(driver)
    page.open()
    page.login("tomsmith", "SuperSecretPassword!")
    assert "You logged into a secure area!" in page.message()

Папка core/ — базовые классы и ожидания

Здесь лежит общий код, который используется на всех страницах.

## core/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 click(self, locator):
        el = self.wait.until(EC.element_to_be_clickable(locator))
        el.click()

    def type(self, locator, text):
        el = self.wait.until(EC.visibility_of_element_located(locator))
        el.clear()
        el.send_keys(text)

    def get_text(self, locator):
        return self.wait.until(EC.visibility_of_element_located(locator)).text

Папка data/ — тестовые данные

Все логины, пароли и тестовые значения удобно хранить отдельно:

## data/users.py
VALID = ("tomsmith", "SuperSecretPassword!")
INVALID = [
    ("tomsmith", "wrong"),
    ("", "SuperSecretPassword!"),
    ("tomsmith", ""),
]

Папка utils/ — утилиты

Общие функции, не зависящие от страниц и тестов.

## utils/files.py
import os, time

def wait_file(path, timeout=10):
    for _ in range(timeout):
        if os.path.exists(path):
            return True
        time.sleep(1)
    return False

Папка reports/ — результаты

Сюда сохраняются отчёты, скриншоты и HTML-файлы с результатами тестов.

pytest --alluredir=reports
allure serve reports

Минимальная структура проекта

selenium-tests/
├── tests/
│   ├── auth/
│   │   └── test_login.py
│   └── conftest.py
├── pages/
│   └── login_page.py
├── core/
│   └── page.py
├── data/
│   └── users.py
├── utils/
│   └── files.py
├── reports/
└── pytest.ini

Такой шаблон подходит для любого проекта на Selenium.

Использование conftest.py и фикстур Pytest

Файл conftest.py — это центральное место, где хранятся общие настройки, фикстуры и хуки. Pytest автоматически видит этот файл, если он находится в папке с тестами или выше по дереву. Благодаря этому фикстуры не нужно импортировать вручную — они доступны во всех тестах проекта.

Что делает фикстура

Фикстура — это функция, которая подготавливает окружение для теста: создаёт браузер, логинится, подключается к базе или очищает данные. После завершения теста фикстура может выполнить обратные действия — закрыть браузер, удалить временные файлы или очистить сессию.

Пример фикстуры для Selenium

## tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture
def driver():
    """Создаёт экземпляр браузера Chrome и закрывает его после теста"""
    opts = Options()
    opts.add_argument("--window-size=1366,768")
    opts.add_argument("--disable-notifications")
    opts.add_argument("--lang=ru-RU")
    driver = webdriver.Chrome(options=opts)

    ## Возврат управления тесту
    yield driver

    ## Действия после завершения теста
    driver.quit()

Теперь любой тест может просто принять аргумент driver, и Pytest сам подставит туда экземпляр браузера.

## tests/test_example.py
def test_open_page(driver):
    driver.get("https://example.com")
    assert "Example" in driver.title

Фикстура создаёт браузер перед тестом и закрывает его автоматически после завершения.

Область действия фикстуры

Pytest позволяет задавать, как часто создаётся фикстура — это называется scope. Значения:

  • "function" — создаётся для каждого теста (по умолчанию);
  • "class" — один раз для всех тестов в классе;
  • "module" — один раз для всех тестов в файле;
  • "session" — один раз на всю сессию тестов.
@pytest.fixture(scope="session")
def base_url():
    """Базовый адрес тестируемого сайта"""
    return "https://the-internet.herokuapp.com"

Вложенные фикстуры

Фикстура может использовать другую фикстуру. Например, можно отделить создание браузера от открытия сайта:

@pytest.fixture
def browser():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

@pytest.fixture
def login_page(browser, base_url):
    """Открывает страницу логина перед тестом"""
    browser.get(base_url + "/login")
    return browser

В тесте можно использовать только login_page, и браузер создастся автоматически.

def test_title(login_page):
    assert "Login" in login_page.title

Автоматический запуск (autouse)

Если фикстура нужна всем тестам, можно задать autouse=True. Тогда её не придётся передавать в аргументах.

@pytest.fixture(autouse=True)
def setup_environment(tmp_path):
    """Создаёт временную папку перед каждым тестом"""
    print(f"Temp dir: {tmp_path}")

Передача параметров

Фикстуры можно параметризовать. Например, запускать тесты в разных браузерах:

@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
    if request.param == "chrome":
        from selenium.webdriver import Chrome
        driver = Chrome()
    else:
        from selenium.webdriver import Firefox
        driver = Firefox()
    yield driver
    driver.quit()

Pytest выполнит каждый тест по одному разу на каждом браузере.

Хуки и отчёты

В conftest.py можно добавлять хуки — специальные функции, которые Pytest вызывает в определённые моменты. Например, можно сохранять скриншот при падении теста:

import pathlib

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
   outcome = yield
   rep = outcome.get_result()
   if rep.when == "call" and rep.failed:
       driver = item.funcargs.get("driver")
       if driver:
           out = pathlib.Path("reports/screenshots")
           out.mkdir(parents=True, exist_ok=True)
           name = out / f"{item.name}.png"
           driver.save_screenshot(str(name))

Теперь при любой ошибке Selenium сделает снимок экрана, а Pytest положит его в папку reports/screenshots.

Пример структуры

tests/
├── conftest.py
├── auth/
│   └── test_login.py
└── profile/
    └── test_edit.py

Фикстуры в conftest.py автоматически доступны всем тестам. Если добавить conftest.py в каждую подпапку, можно создавать локальные фикстуры только для этой области (например, отдельные данные для блока профиля).

Пример полного цикла

## tests/auth/test_login.py
def test_login(driver, base_url):
   driver.get(base_url + "/login")
   driver.find_element("id", "username").send_keys("tomsmith")
   driver.find_element("id", "password").send_keys("SuperSecretPassword!")
   driver.find_element("css selector", "button").click()
   assert "/secure" in driver.current_url

Так conftest.py становится центром управления проектом: здесь создаются браузеры, подключаются данные, формируются отчёты и логика запуска тестов.

Инициализация WebDriver в фикстурах

При работе с Selenium в каждом тесте нужно запускать браузер, открывать страницы и корректно завершать сессию. Делать это вручную в каждом файле неудобно — код повторяется, тесты становятся громоздкими. Поэтому инициализацию браузера выносят в фикстуру, обычно в файл conftest.py.

Базовая инициализация

Простейший пример — создание экземпляра Chrome перед тестом и закрытие после выполнения:

## tests/conftest.py
import pytest
from selenium import webdriver

@pytest.fixture
def driver():
    ## Создание экземпляра браузера
    driver = webdriver.Chrome()

    ## Возврат драйвера в тест
    yield driver

    ## После завершения теста — закрытие браузера
    driver.quit()

Теперь тест просто принимает driver как аргумент — Pytest сам подставит созданный экземпляр браузера:

## tests/test_example.py
def test_open_page(driver):
    driver.get("https://example.com")
    assert "Example" in driver.title

Это избавляет от повторяющегося кода и гарантирует, что браузер всегда закроется даже при падении теста.

Добавление опций к драйверу

Чтобы сделать тесты стабильнее, в фикстуру часто добавляют параметры браузера: размер окна, язык, headless-режим, отключение уведомлений.

from selenium.webdriver.chrome.options import Options

@pytest.fixture
def driver():
    options = Options()
    options.add_argument("--window-size=1366,768")
    options.add_argument("--disable-notifications")
    options.add_argument("--lang=ru-RU")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()

Что это даёт:

  • headless — браузер работает без интерфейса, удобно для CI;
  • window-size — одинаковая ширина и высота окна во всех тестах;
  • disable-notifications — отключает всплывающие запросы;
  • lang — задаёт язык интерфейса, чтобы сайт открывался на нужной локали.

Передача базового URL

Иногда тестам нужно знать, где расположен тестируемый сайт. Это можно задать в отдельной фикстуре:

@pytest.fixture(scope="session")
def base_url():
    return "https://the-internet.herokuapp.com"

Тогда тест можно записать так:

def test_login_page(driver, base_url):
    driver.get(base_url + "/login")
    assert "Login" in driver.title

Параметризация фикстуры для разных браузеров

Если проект тестируется на нескольких браузерах, фикстура может принимать параметр request.param и запускать нужный вариант:

@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
    if request.param == "chrome":
        from selenium.webdriver import Chrome
        driver = Chrome()
    else:
        from selenium.webdriver import Firefox
        driver = Firefox()
    yield driver
    driver.quit()

Pytest выполнит каждый тест по одному разу на каждом браузере.

Инициализация через setup_class (если нужен класс)

Иногда тесты организуют в класс, тогда фикстуру можно объявить с областью scope="class" — она создаст браузер один раз для всех тестов внутри класса:

@pytest.fixture(scope="class")
def driver_class(request):
    driver = webdriver.Chrome()
    request.cls.driver = driver
    yield
    driver.quit()

Использование:

@pytest.mark.usefixtures("driver_class")
class TestLogin:
    def test_open(self):
        self.driver.get("https://example.com")
        assert "Example" in self.driver.title

Безопасное закрытие браузера

Чтобы избежать ошибок, если браузер уже закрыт или тест упал раньше, можно добавить обработку исключений:

@pytest.fixture
def driver():
    driver = webdriver.Chrome()
    yield driver
    try:
        driver.quit()
    except Exception:
        pass

Так тесты не прер��утся из-за сбоя при завершении сессии.

Пример полного conftest.py

## tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture(scope="session")
def base_url():
    return "https://the-internet.herokuapp.com"

@pytest.fixture
def driver():
    opts = Options()
    opts.add_argument("--window-size=1366,768")
    opts.add_argument("--disable-notifications")
    opts.add_argument("--headless=new")
    driver = webdriver.Chrome(options=opts)
    yield driver
    try:
        driver.quit()
    except Exception:
        pass

Фикстура для инициализации WebDriver — основа всех автотестов на Selenium.

Повторное использование шагов и утилит

Когда тестов становится десятки или сотни, в коде начинают повторяться одни и те же действия: открыть страницу, авторизоваться, загрузить файл, кликнуть по кнопке. Решение — вынести повторяющиеся части в отдельные функции, классы и утилиты. Это делает тесты короче, понятнее и облегчает поддержку.

Шаги как функции

Любое действие, которое встречается в нескольких тестах, стоит оформить как функцию.

## utils/steps.py
from selenium.webdriver.common.by import By

def login(driver, username, password):
    driver.get("https://the-internet.herokuapp.com/login")
    driver.find_element(By.ID, "username").send_keys(username)
    driver.find_element(By.ID, "password").send_keys(password)
    driver.find_element(By.CSS_SELECTOR, "button").click()

Теперь тест становится проще:

## tests/auth/test_login.py
from utils.steps import login

def test_login_success(driver):
    login(driver, "tomsmith", "SuperSecretPassword!")
    assert "/secure" in driver.current_url

Такой приём уменьшает дублирование и делает код читаемым: шаги можно переиспользовать в любом тесте.

Повторяющиеся проверки

Проверки часто одинаковые: убедиться, что элемент виден, текст совпадает, URL содержит подстроку. Их тоже удобно оформить как функции.

## utils/asserts.py
from selenium.webdriver.common.by import By

def assert_visible(driver, locator):
    els = driver.find_elements(*locator)
    assert els and els[0].is_displayed(), f"Элемент {locator} не найден или скрыт"

def assert_text_contains(driver, locator, expected):
    el = driver.find_element(*locator)
    assert expected in el.text, f"Ожидали '{expected}', получили '{el.text}'"

Теперь тест выглядит как инструкция:

from utils.asserts import assert_visible, assert_text_contains
from selenium.webdriver.common.by import By

def test_profile_loaded(driver):
    driver.get("https://example.com/profile")
    assert_visible(driver, (By.CSS_SELECTOR, ".profile-header"))
    assert_text_contains(driver, (By.CSS_SELECTOR, ".username"), "Tom")

Шаги в виде классов

Если шагов становится много, удобно объединять их в классы — по смыслу или по разделу сайта.

## steps/auth_steps.py
from selenium.webdriver.common.by import By

class AuthSteps:
    def __init__(self, driver):
        self.driver = driver

    def open_login(self):
        self.driver.get("https://the-internet.herokuapp.com/login")

    def login(self, user, pwd):
        self.driver.find_element(By.ID, "username").send_keys(user)
        self.driver.find_element(By.ID, "password").send_keys(pwd)
        self.driver.find_element(By.CSS_SELECTOR, "button").click()

    def logout(self):
        self.driver.find_element(By.CSS_SELECTOR, "a[href='/logout']").click()

Использование:

from steps.auth_steps import AuthSteps

def test_logout(driver):
    auth = AuthSteps(driver)
    auth.open_login()
    auth.login("tomsmith", "SuperSecretPassword!")
    auth.logout()
    assert "/login" in driver.current_url

Теперь тесты читаются как последовательность действий пользователя, а сами шаги можно использовать в любых сценариях.

Утилиты для ожиданий и времени

Некоторые задачи повторяются в каждом проекте: подождать появления элемента, проверить наличие файла, замерить время шага. Это тоже можно вынести в общий модуль.

## utils/waits.py
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def wait_visible(driver, locator, timeout=10):
    return WebDriverWait(driver, timeout).until(
        EC.visibility_of_element_located(locator)
    )

def wait_and_click(driver, locator, timeout=10):
    el = WebDriverWait(driver, timeout).until(
        EC.element_to_be_clickable(locator)
    )
    el.click()

def sleep_short():
    time.sleep(0.5)

Теперь в тестах можно использовать короткие вызовы:

from utils.waits import wait_and_click
from selenium.webdriver.common.by import By

def test_submit_form(driver):
    driver.get("https://example.com/form")
    wait_and_click(driver, (By.CSS_SELECTOR, "button.submit"))

Общие фикстуры

Иногда полезно объединить шаги и утилиты в фикстуры — чтобы подготовка окружения происходила автоматически.

## tests/conftest.py
import pytest
from utils.steps import login

@pytest.fixture
def authorized_user(driver):
    login(driver, "tomsmith", "SuperSecretPassword!")
    yield driver
    driver.get("https://the-internet.herokuapp.com/logout")

Теперь любой тест, которому нужен авторизованный пользователь, просто использует authorized_user:

def test_open_secure_page(authorized_user):
    assert "/secure" in authorized_user.current_url

Пример структуры проекта

selenium-tests/
├── tests/
│   ├── auth/
│   │   └── test_login.py
│   ├── profile/
│   │   └── test_edit.py
│   └── conftest.py
├── steps/
│   └── auth_steps.py
├── utils/
│   ├── asserts.py
│   ├── waits.py
│   └── steps.py

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845