Selenium
Selenium — мощный инструмент автоматизации браузера: UI-тесты, парсинг динамических сайтов и рутинные действия в вебе на Python.
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()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 нужен, когда:
- контент рендерится JavaScript (SPA).
- нужны клики/скроллы для появления данных.
- тестируешь реальное поведение UI.
10. Лучшие практики
- Селекторы по data-testid вместо хрупких XPath.
- Держи драйвер в finally/контексте — закрывай после прогона.
- Headless в CI, с окном — при локальной отладке.
- Не храни пароли в тестах — используй переменные окружения.
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">выполняется через send_keys с путём к файлу; - скачивание проверяйте по появлению файла в папке, а не только по клику;
- для IE/реальных диалогов подключите
Selenium Wireили robot-клавиши.
<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()| Элемент | Команда |
|---|---|
| iframe | switch_to.frame / default_content |
| новое окно | switch_to.window(handle) |
| alert / confirm / prompt | switch_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")- используйте тестовые базы и фикстуры, а не продакшен-данные;
- запускайте тесты в CI (GitHub Actions, Jenkins) с headless-браузером;
- селекторы по data-атрибутам (
data-testid) переживают вёрстку.
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- один локатор — одно место: вёрстка меняется в одном классе;
- методы страницы говорят на языке бизнеса (login, add_to_cart);
- ожидания держите внутри методов страницы, а не в тесте.
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-выражений: тесты станут заметно устойчивее.
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, сетевые логи и видео шагов.
- page_source после падения — мгновенный снимок DOM в момент ошибки;
- скриншот в teardown (см. раздел 4) фиксирует визуальное состояние;
- Har (сетевой журнал) показывает, какие запросы ушли и что ответила заглушка;
- видео продвинутых сред собирают весь прогон — смотрите его в CI-артефактах.
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-поломку видно с первого взгляда.
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 и случайных таймингов.
20. Мобильная автоматизация: Appium
Appium использует WebDriver-протокол для мобильных приложений: те же driver.find_element и waits, но для iOS и Android. Управление идёт через тот же тестовый фреймворк, что и веб.
desired_caps = { platformName: 'Android', deviceName: 'emulator',
appPackage: 'com.app.pkg', appActivity: '.Main',
automationName: 'UiAutomator2' }- на iOS понадобится macOS и XCUITest-драйвер;
- эмулятор быстрее реального устройства для прогона, физика — проверка жестов и Network;
- жесты (swipe, pinch, longPress) — API W3C Actions из раздела 2;
- переключения приложений и пушей: driver.check_permissions и тест-фоновые действия.
| Платформа | Behat/драйвер |
|---|---|
| Android | UiAutomator2 |
| iOS | XCUITest |
| гибрид/webview | переключение на WebView-контекст |
21. Тест-данные и окружение
Стабильные тесты требуют управления данными: фронтовые сценарии зависят от того, что лежит в БД. Четыре правила тест-данных экономят десятки часов:
- каждый тест создаёт свои данные и чистит их (фикстуры scope='function');
- данные — через API резервного канала, не через UI;
- параллельные прогоны изолируются генерацией уникальных суффиксов;
- staging и prod разделены: тесты никогда не пишут в прод.
@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) |
Что дальше
Тестовые скиллсы дополнит pytest, а REST API-тесты — Karate. Инфраструктуру запуска даст CI в GitLab.