Python: Selenium

Теория: Работа с ошибками

Работа с ошибками — Скриншот при падении теста

При автоматизации тестов важно не просто определить, что тест упал, но и понять, почему это произошло. Иногда текст ошибки Pytest или Selenium не даёт полного ответа. Визуальный снимок страницы помогает увидеть, что происходило в браузере в момент сбоя: не прогрузился элемент, исчез баннер, появилось модальное окно или всплыла неожиданная ошибка.

Сделать скриншот в Selenium очень просто — драйвер имеет встроенный метод save_screenshot(). Он сохраняет текущее состояние страницы в виде PNG-файла.

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://the-internet.herokuapp.com/login")

try:
    ## Преднамеренно ищем несуществующий элемент
    driver.find_element(By.ID, "wrong-id")
except Exception as e:
    ## При ошибке сохраняем скриншот
    driver.save_screenshot("error.png")
    print("Ошибка:", e)

driver.quit()

Если тест не смог найти элемент с ID wrong-id, Selenium выбросит исключение NoSuchElementException. Код внутри блока except поймает ошибку и сделает снимок экрана error.png в корне проекта. На изображении будет ровно то, что видел браузер в момент ошибки.

Скриншот при падении теста в Pytest

В реальных проектах ручное добавление try/except в каждый тест неудобно. Вместо этого используют фикстуру Pytest, которая автоматически делает скриншот при любой ошибке.

import pytest
from selenium import webdriver

@pytest.fixture
def browser(request):
    driver = webdriver.Chrome()
    yield driver
    ## Если тест завершился с ошибкой — сохраняем скриншот
    if request.node.rep_call.failed:
        test_name = request.node.name
        driver.save_screenshot(f"screenshots/{test_name}.png")
    driver.quit()

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    ## Позволяет фикстуре знать, упал ли тест
    outcome = yield
    result = outcome.get_result()
    setattr(item, "rep_" + result.when, result)

Теперь достаточно использовать фикстуру browser в любом тесте:

def test_example(browser):
    browser.get("https://example.com")
    assert "NotExistingText" in browser.page_source

Если проверка не пройдёт, Pytest создаст папку screenshots/ и сохранит снимок страницы с именем test_example.png.

Зачем нужны скриншоты

Скриншот — это быстрый способ визуальной диагностики. По нему можно понять:

  • элемент действительно отсутствует на странице;
  • сайт не загрузился (пустой экран, ошибка 500 или 404);
  • появилось всплывающее окно, перекрывающее кнопку;
  • страница открылась не та (неправильный редирект).

В отличие от логов, скриншот показывает реальное состояние браузера, что особенно полезно при нестабильных интеграционных тестах.

Скриншоты и Allure

Если используется отчётность Allure, скриншоты можно прикреплять прямо в отчёт. Для этого в тест добавляют вызов allure.attach().

import allure

try:
    element = browser.find_element(By.ID, "unknown")
except Exception:
    allure.attach(
        browser.get_screenshot_as_png(),
        name="Ошибка",
        attachment_type=allure.attachment_type.PNG
    )
    raise

В отчёте Allure рядом с упавшим тестом появится вкладка с изображением страницы. Это позволяет анализировать ошибки прямо в интерфейсе отчёта.

Где хранить скриншоты

Хранить их можно в отдельной папке проекта, например screenshots/, или внутри отчётов Allure. Обычно к имени файла добавляют дату и время, чтобы не перезаписывать старые снимки:

from datetime import datetime

timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
driver.save_screenshot(f"screenshots/error_{timestamp}.png")

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

Работа с ошибками — Логирование ошибок

Когда тест падает, скриншота часто недостаточно. Он показывает, что случилось на экране, но не объясняет, почему. Для этого в автотестах используется логирование — систематическая запись событий, действий и ошибок, которые происходят во время выполнения теста. Логи позволяют восстановить ход теста, понять, где он остановился, и увидеть причины сбоя без повторного запуска.

Что такое логирование в тестах

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

Базовый инструмент для этого — стандартный модуль Python logging. Он встроен в язык и не требует установки дополнительных библиотек.

Простой пример логирования

import logging
from selenium import webdriver
from selenium.webdriver.common.by import By

## Настраиваем логирование
logging.basicConfig(
    filename="test_log.txt",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

driver = webdriver.Chrome()

logging.info("Открываем страницу входа")
driver.get("https://the-internet.herokuapp.com/login")

try:
    logging.info("Вводим имя пользователя")
    driver.find_element(By.ID, "username").send_keys("tomsmith")

    logging.info("Вводим пароль")
    driver.find_element(By.ID, "password").send_keys("SuperSecretPassword!")

    logging.info("Нажимаем кнопку входа")
    driver.find_element(By.CSS_SELECTOR, "button.radius").click()

    logging.info("Проверяем URL после входа")
    assert "/secure" in driver.current_url

    logging.info("Авторизация прошла успешно")

except Exception as e:
    logging.error(f"Ошибка при выполнении теста: {e}")
    driver.save_screenshot("error.png")

finally:
    driver.quit()
    logging.info("Браузер закрыт, тест завершён")

После выполнения в папке проекта появится файл test_log.txt, где будет записан полный ход теста. Пример содержимого:

2025-11-09 15:31:12 [INFO] Открываем страницу входа
2025-11-09 15:31:14 [INFO] Вводим имя пользователя
2025-11-09 15:31:14 [INFO] Вводим пароль
2025-11-09 15:31:15 [INFO] Нажимаем кнопку входа
2025-11-09 15:31:16 [INFO] Проверяем URL после входа
2025-11-09 15:31:17 [INFO] Авторизация прошла успешно
2025-11-09 15:31:17 [INFO] Браузер закрыт, тест завершён

Если тест упадёт, в логах появится строка с уровнем ERROR и текстом исключения.

Уровни логов

Модуль logging поддерживает пять уровней сообщений:

  • DEBUG — подробные технические детали (редко выводятся в отчётах).
  • INFO — основные шаги теста, которые позволяют понять, что происходит.
  • WARNING — предупреждения, не критичные, но требующие внимания.
  • ERROR — ошибки, из-за которых тест не может продолжиться.
  • CRITICAL — фатальные сбои, после которых тест аварийно завершён.

Обычно в автотестах достаточно уровней INFO и ERROR. Первый фиксирует шаги теста, второй — причины падений.

Логирование в связке с Pytest

Pytest может автоматически перехватывать логи и добавлять их в отчёты. Если запустить тест с параметром -s, все логи выводятся прямо в консоль:

pytest -s tests/test_login.py

Если нужно писать логи в файл, добавляется конфигурация через фикстуру.

import logging
import pytest

@pytest.fixture(autouse=True)
def setup_logging():
    logging.basicConfig(
        filename="logs/tests.log",
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s"
    )
    yield
    logging.info("=== Тест завершён ===")

Теперь все тесты будут автоматически записывать логи в logs/tests.log.

Пример интеграции с Allure

Если используется Allure, можно прикреплять логи прямо к отчёту. Это особенно полезно для CI/CD, где нет доступа к файловой системе.

import allure

try:
    ## Шаг, который может упасть
    driver.find_element(By.ID, "unknown")
except Exception as e:
    allure.attach(
        str(e),
        name="Ошибка",
        attachment_type=allure.attachment_type.TEXT
    )
    allure.attach(
        driver.get_log("browser"),
        name="Browser console",
        attachment_type=allure.attachment_type.TEXT
    )
    raise

В отчёте появятся вкладки с текстом исключения и логами браузера. Это помогает понять, что именно пошло не так — баг на стороне фронтенда или ошибка в логике теста.

Логи браузера

Кроме логов теста, Selenium умеет получать сообщения из консоли браузера. Это позволяет фиксировать JavaScript-ошибки или проблемы с загрузкой.

logs = driver.get_log("browser")
for entry in logs:
    print(entry)

Если в коде сайта возникла ошибка TypeError или 404 Not Found, она появится в этих логах.

Формирование структуры логов

В больших проектах логи делят на уровни и файлы:

  • tests.log — ход выполнения тестов;
  • errors.log — только ошибки и исключения;
  • debug.log — подробные внутренние данные, если нужно отладить.

Для этого можно создать несколько логгеров:

logger = logging.getLogger("test")
error_logger = logging.getLogger("errors")

handler = logging.FileHandler("logs/errors.log")
error_logger.addHandler(handler)
error_logger.setLevel(logging.ERROR)

Теперь в errors.log будут сохраняться только записи уровня ERROR и выше.

Как использовать логи при анализе

  1. Проверить, в каком месте тест остановился.
  2. Найти последний успешный шаг.
  3. Посмотреть стек исключения в логах.
  4. Сравнить со скриншотом, чтобы понять, совпадает ли визуальная и фактическая ошибка.

Например, если в логах написано NoSuchElementException: Unable to locate element: [id="submit"], а на скриншоте видно всплывающее окно — причина ясна: модалка перекрыла кнопку.

Автоматическое создание логов в CI

При интеграции тестов в CI (например, GitHub Actions, GitLab CI или Jenkins) логи автоматически сохраняются как артефакты сборки. Это удобно: после запуска тестов можно скачать архив logs.zip и посмотреть результаты, не поднимая браузер вручную.

Что такое стек-трейс

Когда автотест падает, в терминале появляется длинная простыня текста.

E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":".login-btn"}
E         (Session info: chrome=129.0.6668.100)
E       Stacktrace:
E         File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/remote/errorhandler.py", line 242, in check_response
E           raise exception_class(message, screen, stacktrace)
E         File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/remote/webdriver.py", line 830, in find_element
E           return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
E         File "/tests/test_login.py", line 12, in test_login_button
E           driver.find_element(By.CSS_SELECTOR, ".login-btn").click()
E       AssertionError: элемент .login-btn не найден

Это и есть стек-трейс — последовательность вызовов функций, которые привели к ошибке. В нём указаны файлы, строки и имена функций. Нижние строки показывают самое глубокое место падения, а верхние — путь до него. Чтобы разобраться, что пошло не так, обычно хватает последних десяти строк: там виден ваш тест и вызовы Selenium, а не внутренние методы библиотек.

Форматы стека

Когда тест падает, Pytest выводит стек — список строк с файлами и функциями, которые вызывались до ошибки. Чтобы управлять длиной этого списка, у Pytest есть параметр --tb. Он означает traceback, то есть «обратная трассировка».

Если запустить тест с --tb=short, Pytest покажет только последние строки — то, где тест реально упал:

pytest --tb=short

Пример вывода:

E       AssertionError: кнопка "Войти" не найдена

Такой формат подходит, если нужно просто увидеть, что не сработало.

Для подробного анализа используют --tb=long:

pytest -vv --tb=long

Теперь Pytest покажет весь путь — из библиотек Selenium, вашего теста и фикстур.

E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":".login-btn"}
E         File "/tests/test_login.py", line 12, in test_login_button
E           driver.find_element(By.CSS_SELECTOR, ".login-btn").click()

Видно, какая строка вызвала ошибку, какой локатор не найден и какой тип исключения сработал.

Есть и автоматический вариант — --tb=auto. Pytest сам решает, сколько строк показать: короткий стек для простых ошибок, длинный для сложных.

Сообщения в стеке

Чтобы сделать стек понятнее, можно добавить к assert собственный текст. Тогда Pytest выведет сообщение вместе с местом падения.

Пример:

assert "Welcome" in message.text, "Ожидали текст 'Welcome', но его нет на странице"

Вывод будет таким:

E       AssertionError: Ожидали текст 'Welcome', но его нет на странице
E         assert 'Welcome' in 'Ошибка авторизации'

Теперь сразу видно, чего не хватило.

Иногда нужно дополнить стек, если ошибка была перехвачена вручную. Тогда используют конструкцию raise ... from e. Она сохраняет оригинальное место падения и добавляет ваше пояснение:

try:
    driver.find_element(By.ID, "login")
except Exception as e:
    raise Exception("Ошибка входа: элемент login не найден") from e

Теперь в выводе появятся обе части: и место, где упал Selenium, и строка с пояснением.

Отладка через breakpoint

Во время выполнения теста иногда нужно увидеть, что именно происходит в браузере в конкретный момент. Для этого используют встроенную команду breakpoint(). Она ставит тест «на паузу» и открывает интерактивную консоль прямо внутри запущенного процесса.

Когда выполнение доходит до строки с breakpoint(), Pytest приостанавливает работу. В консоли можно вводить команды, проверять значения переменных, просматривать DOM и адрес страницы. Это удобно, если элемент не находится, а нужно понять — есть ли он в HTML, правильно ли открылся нужный URL, не перекрыт ли чем-то интерфейс.

Чтобы Pytest позволил работать с консолью, тест запускают так:

pytest -vv -s

Флаг -s отключает подавление вывода и делает консоль «живой». Если хочется, чтобы отладка включалась автоматически при падении теста, добавляют флаг --pdb:

pytest --pdb

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

Пример теста с точкой остановки

from selenium import webdriver
from selenium.webdriver.common.by import By

def test_debug():
    driver = webdriver.Chrome()
    driver.get("https://the-internet.herokuapp.com/login")
    username = driver.find_element(By.ID, "username")
    breakpoint()  # на этой строке можно исследовать состояние браузера
    username.send_keys("tomsmith")
    driver.quit()

Когда тест остановится, можно выполнять команды прямо в консоли. Полезно проверить:

driver.current_url            # адрес страницы
driver.page_source[:300]      # первые 300 символов HTML
len(driver.find_elements(By.CSS_SELECTOR, "button"))  # количество кнопок

Эти проверки помогают понять, загружена ли нужная страница и виден ли искомый элемент.

Сохранение стека и скриншота

Если ошибка повторяется, а вручную воспроизводить её неудобно, лучше сохранять артефакты автоматически. Для этого используют модуль traceback. Он записывает стек вызовов в лог, а Selenium делает скриншот страницы.

import logging, traceback
from selenium.webdriver.common.by import By
try:
    driver.find_element(By.ID, "missing")
except Exception:
    logging.error("Падение шага:\n%s", traceback.format_exc())
    driver.save_screenshot("screenshots/error.png")
    raise

После падения появятся два файла:

-в логах будет текст ошибки с полным стеком, -в папке screenshots — снимок страницы на момент сбоя.

Автоматическое сохранение при падении

Когда тестов становится десятки, добавлять в каждый блок try/except неудобно. Вместо этого всё можно сделать централизованно — через фикстуру в conftest.py. Фикстура создаёт браузер, передаёт его в тест, а после выполнения проверяет, упал ли сценарий. Если да — сохраняет скриншот и HTML страницы.

import pytest
from selenium import webdriver

@pytest.fixture
def browser(request):
    driver = webdriver.Chrome()
    yield driver
    rep = getattr(request.node, "rep_call", None)
    if rep and rep.failed:
        name = request.node.name
        driver.save_screenshot(f"screenshots/{name}.png")
        with open(f"screenshots/{name}.html", "w", encoding="utf-8") as f:
            f.write(driver.page_source)
    driver.quit()

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    result = outcome.get_result()
    setattr(item, "rep_" + result.when, result)

Теперь при любом падении Selenium автоматически сохраняет два файла — снимок экрана и HTML текущей страницы. HTML можно открыть в браузере и проверить, что элемент действительно отсутствовал или был скрыт под всплывающим окном. Это экономит время и помогает понять причину сбоя без повторного запуска.

Полезные флаги

Когда тестов много, удобно управлять прогоном с помощью флагов Pytest.

  • -x — прерывает выполнение на первом упавшем тесте. Это удобно при отладке, когда важно сразу остановиться и не ждать окончания всех прогонов.
  • --maxfail=1 делает то же, но мягче: фиксирует ограничение по количеству падений, после которых тестирование прекращается.
  • --lf (last-failed) запускает только те тесты, которые упали в прошлый раз. Это ускоряет проверку после исправлений.
  • -k "login and not smoke" позволяет запустить выборку тестов по ключевым словам. Например, прогнать все сценарии логина, кроме тех, что помечены как smoke.

Ожидания вместо ошибок

Одна из частых причин падений — элемент ещё не успел появиться на странице. Selenium пытается кликнуть, но DOM ещё не готов. Это вызывает ошибки NoSuchElementException или ElementClickInterceptedException. Решение — использовать явные ожидания.

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

btn = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
)
btn.click()

В этом примере Selenium ждёт до 10 секунд, пока кнопка не станет кликабельной, и только потом кликает. Это безопаснее, чем просто find_element.

Проверка выборки

Иногда ошибка в стеке выглядит как внутренний сбой Selenium, но настоящая причина — пустая выборка. Проверить это можно просто:

els = driver.find_elements(By.CSS_SELECTOR, ".menu .item")
assert els, "Коллекция пустая: .menu .item"

Если список пуст, значит локатор неверный. Следует подобрать более стабильный селектор, например по id или уникальному классу. Чем точнее локатор, тем меньше случайных падений.

Логи браузера

Иногда тесты падают из-за ошибок в самом приложении, а не в автотестах. Через driver.get_log("browser") можно вывести сообщения из консоли браузера:

for entry in driver.get_log("browser"):
    print(entry["level"], entry["message"])

Если в выводе встречаются TypeError или 404, значит на странице есть JavaScript-ошибка или не загрузился нужный ресурс. Это повод завести баг на фронтенд, а не чинить тест.

Настройка Pytest

Чтобы отчёты были читаемыми, базовые параметры можно задать в файле pytest.ini:

[pytest]
addopts = -vv --tb=auto
log_cli = true
log_cli_level = INFO

Опция --tb=auto делает стек аккуратным: простые ошибки показываются коротко, сложные — с контекстом. log_cli включает вывод логов прямо в терминал во время прогона.

Отладка через PDB

Иногда проще всего поймать проблему вручную. Для этого используется встроенный отладчик Python — PDB. Его можно активировать флагом:

uv run pytest --pdb

Когда тест падает, Pytest открывает консоль с интерактивным управлением. Основные команды:

  • n — перейти к следующей строке,
  • s — шагнуть внутрь функции,
  • c — продолжить выполнение,
  • l — показать участок кода,
  • p var — вывести значение переменной.

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

Работа с ошибками — Повторное выполнение теста (pytest-rerunfailures)

Автотесты должны быть стабильными, но в реальности часть из них падает случайно. Это может быть проблема сети, задержка загрузки страницы, непредсказуемое поведение браузера или нестабильный элемент DOM. Такие падения называют флаки-тестами (flaky tests). Они не указывают на баг в продукте, но портят отчёт, создавая ложные ошибки.

Чтобы минимизировать влияние таких случайных сбоев, в Pytest используется плагин pytest-rerunfailures. Он автоматически повторяет тест, если тот упал, и считает его успешным, если повторное выполнение прошло без ошибок. Это не «маскирует» реальные дефекты, а помогает фильтровать нестабильность окружения.

Установка плагина

Плагин устанавливается одной командой:

uv add pytest-rerunfailures

Проверить, что он активен, можно через список установленных плагинов:

pytest --trace-config

В выводе появится строка pytest_rerunfailures.

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

Чтобы запустить тест с повторным выполнением при падении, используется флаг --reruns.

uv run pytest --reruns 3

Эта команда означает: если какой-то тест упал, Pytest перезапустит его до трёх раз. Если хотя бы один повтор пройдёт успешно, тест засчитывается как «пройденный после N повторов». Если упадёт все три раза — он считается действительно упавшим.

Можно добавить параметр --reruns-delay, чтобы сделать паузу между попытками. Это полезно, если проблема связана с медленной загрузкой страницы.

uv run pytest --reruns 3 --reruns-delay 2

Здесь между каждой попыткой выполняется пауза в 2 секунды.

Пример с флаки-тестом

Иногда элемент не успевает появиться на странице, и тест падает с ошибкой NoSuchElementException.

from selenium import webdriver
from selenium.webdriver.common.by import By

def test_flaky_button():
    driver = webdriver.Chrome()
    driver.get("https://the-internet.herokuapp.com/dynamic_loading/2")

    start = driver.find_element(By.CSS_SELECTOR, "#start button")
    start.click()

    ## Иногда элемент появляется не сразу
    message = driver.find_element(By.ID, "finish")
    assert "Hello World!" in message.text

    driver.quit()

Без ожидания тест может упасть. Но если его перезапустить, он пройдёт. Добавив --reruns 2, мы дадим системе шанс выполнить повтор и подтвердить, что ошибка не постоянная.

Настройка через декораторы

Если повтор нужно задать только для конкретного теста, можно использовать декоратор @pytest.mark.flaky(reruns=N, reruns_delay=D).

import pytest

@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_login(browser):
    browser.get("https://the-internet.herokuapp.com/login")
    browser.find_element(By.ID, "username").send_keys("tomsmith")
    browser.find_element(By.ID, "password").send_keys("SuperSecretPassword!")
    browser.find_element(By.CSS_SELECTOR, "button.radius").click()
    assert "/secure" in browser.current_url

Теперь только этот тест будет перезапущен дважды, если упадёт. Остальные выполняются один раз.

Комбинация с Allure

Если проект использует Allure, повторные прогоны не теряются. В отчёте Allure каждый повтор фиксируется как отдельный шаг с пометкой flaky. Это позволяет видеть, сколько раз тест падал перед успешным выполнением.

Пример запуска:

pytest --reruns 3 --alluredir=reports
allure serve reports

В отчёте видно: «первая попытка — ошибка, вторая — успех». Это полезно для анализа нестабильных тестов и улучшения ожиданий.

Типичные причины флаки-поведения

Повторное выполнение помогает бороться с последствиями, но не с причиной. Часто тесты становятся нестабильными из-за:

  • отсутствия явных ожиданий (элемент не успевает появиться);
  • зависания браузера или JavaScript-ошибок;
  • асинхронных операций на фронтенде;
  • неочищенных данных предыдущего теста;
  • слишком жёстких таймаутов.

Если один и тот же тест падает каждый раз — pytest-rerunfailures не поможет, потому что это реальный баг. Но если сбой редкий, повтор подтвердит стабильность сценария.

Интеграция с CI/CD

В CI-среде (например, Jenkins, GitLab, GitHub Actions) повторный запуск помогает сгладить случайные сетевые сбои. Обычно конфигурацию плагина выносят в pytest.ini:

## pytest.ini
[pytest]
addopts = --reruns 2 --reruns-delay 1

Теперь все тесты при каждом запуске будут повторяться до двух раз при ошибке. Это избавляет от ложных падений при временных проблемах.

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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