Selenium

Selenium — мощный инструмент автоматизации браузера: UI-тесты, парсинг динамических сайтов и рутинные действия в вебе на Python.

Уровень: начальный 17 разделов ⏱ 3–4 часа

1. Что такое Selenium

Selenium WebDriver управляет настоящим браузером (Chrome, Firefox, Edge): открывает страницы, кликает, вводит текст, проверяет результат. Из-за реального окружения идеален для end-to-end тестов.

Языки привязки: Python, Java, C#, JS. Традиционный дуэт с pytest и фреймворком Behave.

2. Установка

pip install selenium

Драйвер браузера: для Chrome подходит chromedriver (Selenium Manager скачает автоматически с версии 4.6+).

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
opts.add_argument('--headless')   # без окна
driver = webdriver.Chrome(options=opts)
driver.get('https://example.com')
print(driver.title)
driver.quit()

3. Поиск элементов

from selenium.webdriver.common.by import By

driver.find_element(By.ID, 'login')
driver.find_element(By.CLASS_NAME, 'btn')
driver.find_element(By.NAME, 'email')
driver.find_element(By.CSS_SELECTOR, 'form > button')
driver.find_element(By.XPATH, '//button[text()="Войти"]')

# множество элементов
items = driver.find_elements(By.CLASS_NAME, 'product')

find_element кидает NoSuchElementException, если ничего не нашли.

4. Действия: клик и ввод

email = driver.find_element(By.ID, 'email')
email.clear()
email.send_keys('a@example.com')

driver.find_element(By.ID, 'submit').click()

Обычно после click() следует ожидание нового состояния страницы.

5. Ожидания

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
btn = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))
btn.click()
Тесты без ожиданий «проскакивают» — элементы еще не отрисовались. Явные ожидания (WebDriverWait) — единственный надёжный путь, sleep не в счёт.

6. Формы, чекбоксы, селекты

checkbox = driver.find_element(By.NAME, 'agree')
checkbox.click()   # отметить

from selenium.webdriver.support.ui import Select
select = Select(driver.find_element(By.NAME, 'city'))
select.select_by_visible_text('Москва')
select.select_by_value('msk')

7. Скриншоты и скролл

driver.save_screenshot('page.png')
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')

8. Сборка мини-теста с pytest

def test_login():
    driver.get('https://demo.example/login')
    driver.find_element(By.ID, 'email').send_keys('a@example.com')
    driver.find_element(By.ID, 'password').send_keys('secret')
    driver.find_element(By.ID, 'submit').click()
    assert WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CLASS_NAME, 'profile')))
    driver.quit()

Полную механику фреймворков — в уроке pytest.

9. HTTP-парсинг против Selenium

Если сайт отдаёт данные по HTTP (JSON/API) — используй requests, это в 10 раз быстрее. Selenium нужен, когда:

10. Лучшие практики

Сложные сценарии проще всего отлаживать со скриншотами на каждом шаге и паузой: driver.implicitly_wait или ручной дебаг с видимым окном.

11. Работа с файлами и загрузками

Реальные сценарии включают загрузку файла на сервер и скачивание отчётов. Selenium умеет и то, и другое — но с особенностями разных браузеров.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

opts = Options()
prefs = {
    "download.default_directory": r"C:\tmp\downloads",
    "download.prompt_for_download": False,
}
opts.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=opts)

driver.get("https://example.com/upload")
driver.find_element(By.NAME, "file").send_keys(r"C:\tmp\report.csv")
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
<input type="file"> — единственный безопасный путь загрузки. Ход по клику с ручным диалогом нестабилен и ломается в CI-окружениях.

12. Фреймы, всплывающие окна и alert

Страницы содержат встроенные фреймы (<iframe>) и диалоги. Selenium держит фокус на одном контексте, поэтому переключения обязательны явно.

# по индексу, имени или WebElement
driver.switch_to.frame(0)
driver.switch_to.frame("frameName")
driver.switch_to.frame(driver.find_element(By.ID, "frameId"))
# назад из фрейма
driver.switch_to.default_content()

# всплывающее окно
main_window = driver.current_window_handle
driver.find_element(By.LINK_TEXT, "Открыть").click()
for handle in driver.window_handles:
    if handle != main_window:
        driver.switch_to.window(handle)

# alert
alert = driver.switch_to.alert
print(alert.text)
alert.accept()   # или alert.dismiss()
ЭлементКоманда
iframeswitch_to.frame / default_content
новое окноswitch_to.window(handle)
alert / confirm / promptswitch_to.alert + accept/dismiss
После переключения окна возвращайтесь к главному через driver.switch_to.window(driver.window_handles[0]).

13. Лучшие практики и CI

Стабильный набор тестов требует дисциплины: изолированные данные, явные ожидания и запуск в контейнерах. Тогда тесты работают и в CI, и локально.

# pytest + Selenium
import pytest
from selenium.webdriver.support.ui import WebDriverWait

@pytest.fixture
def driver():
    d = webdriver.Chrome()
    d.implicitly_wait(5)
    yield d
    d.quit()

def test_login(driver):
    driver.get("http://localhost:8000/login")
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "email"))
    )
    driver.find_element(By.ID, "email").send_keys("test@mail.ru")
Стабильность важнее числа тестов: 50 надёжных проверок защищают лучше, чем 200 «иногда падающих». В CI логируйте скриншот при падении.

14. Page Object Model

Голый тест с сотней find_element хрупок: любое изменение вёрстки чинится сразу в десятке мест. Page Object Model (POM) превращает каждую страницу в класс: локаторы и действия собраны в одном месте, тесты остаются читаемыми.

# pages/login_page.py
class LoginPage:
    EMAIL    = (By.ID, "email")
    PASSWORD = (By.ID, "password")
    SUBMIT   = (By.ID, "submit")

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

    def login(self, email, password):
        self.driver.find_element(*self.EMAIL).send_keys(email)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.SUBMIT).click()
# test_auth.py — тест говорит на языке бизнеса
page = LoginPage(driver)
page.login("test@mail.ru", os.environ["PASSWORD"])
assert "Профиль" in driver.page_source
POM окупается примерно с пятой страницы. Для коротких проверок методы прямо в фикстуре — тоже нормально.

15. Параллельный запуск и Selenium Grid

Набор из 200 тестов в один поток выполняется часами. Решение — параллельный запуск: несколько воркеров делят тесты между собой, каждый со своим браузером.

# pytest-xdist: четыре потока
pytest -n 4

# Selenium Grid: удалённые ноды
from selenium.webdriver.remote.webdriver import WebDriver
driver = WebDriver(command_executor="http://hub:4444", options=opts)

Grid поднимается как набор Docker-контейнеров: hub распределяет тесты между нодами с Chrome, Firefox и Edge. Перед запуском позаботьтесь о независимости тестов: каждый создаёт свои данные и не полагается на порядок выполнения.

# docker-compose.yml (фрагмент)
services:
  hub:    { image: selenium/hub:latest,    ports: ["4444:4444"] }
  chrome: { image: selenium/node-chrome:latest, depends_on: [hub] }
Распараллеливание не «лечит» падающий тест: сначала доведите стабильность, потом ускоряйте. Иначе число флейков растёт вместе с числом воркеров.

16. Shadow DOM: элементы внутри компонентов

Веб-компоненты прячут внутренности в Shadow DOM: обычный find_element внутри них ничего не найдёт, потому что селекторы работают только с обычным световым DOM.

# Selenium 4: прямой доступ к открытому shadow root
host = driver.find_element(By.CSS_SELECTOR, "custom-widget")
shadow = host.shadow_root
inner = shadow.find_element(By.ID, "title")

# вложенные уровни — цепочкой
level2 = inner.shadow_root.find_element(By.CLASS_NAME, "btn")
# закрытый shadow root — только через JS
text = driver.execute_script(
    "return document.querySelector('custom-widget')"
    ".shadowRoot.querySelector('#title').textContent")

С закрытым root предпочитайте data-testid на хосте компонента вместо хрупких JS-выражений: тесты станут заметно устойчивее.

Элемент есть в разметке, но не находится? Первым делом проверьте shadow-дерево в DevTools: Elements → точки проникновения.

17. Флейки: как сделать тесты стабильными

Флейк — тест, который то проходит, то падает без изменений кода. Он обесценивает весь набор: на падения перестают смотреть. Главные причины — гонки, состояние из прошлого запуска и зависимость от локали.

# ожидание исчезновения «скелетона» вместо фиксированного sleep
wait.until(EC.invisibility_of_element_located((By.ID, "loader")))

# скриншот и состояние страницы при падении
@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    if d.current_url != "data:,":
        d.save_screenshot("logs/last_failure.png")
    d.quit()
Источник флейкаПриём
нестабильная сетьWebDriverWait на сетевое условие
данные из прошлого прогонасвежая БД или уникальный префикс
TimeZone и локалификсированные переменные окружения в CI
анимации и скролложидание стабильности размера элемента
Перезапустить упавший тест «для проверки» — плохая практика: вы прячете причину. Доведите тест до трёх зелёных прогонов локально, и только потом заливайте.

18. Отладка проваленных тестов

Красный тест без диагностики — это догадка. У Selenium есть приёмы, которые превращают падение в понятную историю: снимки DOM, сетевые логи и видео шагов.

def test_login(browser):
    try:
        browser.find_element(By.ID, 'login').click()
    except Exception:
        html = browser.page_source
        Path('failure.html').write_text(html, encoding='utf-8')
        raise

Соберите в один failure-report разом: скриншот + page_source + console logs браузера — тогда почти любую flaky-поломку видно с первого взгляда.

Console logs — скрытый источник: «Failed to load» и JS-ошибки приходят раньше Selenium. Логируйте их в отчёте на каждом шаге.
page_source не включает слушателей, созданных тестом, и динамику fetch. Для SPA после падения дайте паузу-ожидание (раздел 3), иначе снимок поймает «момент до».

19. Альтернативы: Playwright и WebdriverIO

Selenium — стандарт, но не единственный. Playwright и WebdriverIO используют те же концепции ({browser, locator, wait}) с другим акцентом, и команды часто мигрируют.

ИнструментСильные стороны
Seleniumвсе браузеры, огромное комьюнити, WebDriver-стандарт
Playwrightавтовейты, network interception, без webdriver
WebdriverIOинтеграция с Mocha/Jasmine, мобильные
// Playwright выполняет действия почти без явных wait
await page.goto('https://site.ru/login');
await page.getByLabel('Логин').fill('user');
await expect(page.getByRole('heading', { name: 'Привет' })).toBeVisible();

Ключевое отличие Playwright: автоожидание по умолчанию и перехват запросов — тесты перестают страдать от ручных WebDriverWait и случайных таймингов.

См. курс Веб-разработка: выбор инструмента зависит от проекта — для SPA на React/Vue Playwright часто быстрее строит стабильные тесты.
Миграция не переименование: параллельно держите оба набора как можно дольше, сравнивая flakiness на одном CI.

20. Мобильная автоматизация: Appium

Appium использует WebDriver-протокол для мобильных приложений: те же driver.find_element и waits, но для iOS и Android. Управление идёт через тот же тестовый фреймворк, что и веб.

desired_caps = { platformName: 'Android', deviceName: 'emulator',
                  appPackage: 'com.app.pkg', appActivity: '.Main',
                  automationName: 'UiAutomator2' }
ПлатформаBehat/драйвер
AndroidUiAutomator2
iOSXCUITest
гибрид/webviewпереключение на WebView-контекст
Эмулятор не повторяет поведение сети и жестов. Один прогон на реальном устройстве в ночном прогоне — обязательный минимум для мобильных релизов.

21. Тест-данные и окружение

Стабильные тесты требуют управления данными: фронтовые сценарии зависят от того, что лежит в БД. Четыре правила тест-данных экономят десятки часов:

@pytest.fixture
def user(client):
    u = api.create_user(tag=uuid4().hex[:8])
    yield u
    api.delete_user(u.id)   # очистка в teardown
ОкружениеДля чего
devлокальный прогон, быстрые итерации
stagingполные ночные прогоны, тест-данные
prodтолько smoke-прогоны (read-only)
Чистка в teardown важнее создания: мусорные данные накапливаются и сами становятся причиной flaky-прогонов — следите за ними в CI.
Никогда не указывайте в тестах реальные данные клиентов. Анонимизация и подмена номеров — обязательный этап настройки staging-окружения.

Что дальше

Тестовые скиллсы дополнит pytest, а REST API-тесты — Karate. Инфраструктуру запуска даст CI в GitLab.

← Вернуться к курсам