Первый тест: открыть страницу и проверить заголовок

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

Автотест = подготовка (открыли браузер) + действие (перешли на страницу) + проверка (assert) + уборка (закрыли браузер). Нет проверки — нет теста, есть просто скрипт, который куда-то ходит.

Проверка заголовка страницы — самый скромный тест, который можно придумать, и при этом абсолютно рабочий smoke-тест: если <title> тот, что ожидали, значит браузер запустился, сеть жива, сервер ответил, страница отрендерилась и это та самая страница, а не «502 Bad Gateway» и не форма входа, куда нас редиректнуло. Такой тест первым ставят в CI и первым смотрят, когда всё красное.

Полный скрипт

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


def test_example_page_title():
    options = Options()
    options.add_argument("--headless=new")   # запуск без окна браузера

    driver = webdriver.Chrome(options=options)
    try:
        driver.get("https://example.com/")

        expected = "Example Domain"
        actual = driver.title
        assert actual == expected, f"Ожидали заголовок '{expected}', получили '{actual}'"

        print("OK:", actual)
    finally:
        driver.quit()   # выполнится даже если assert упал


if __name__ == "__main__":
    test_example_page_title()

Результат:

OK: Example Domain

Построчный разбор

Создание драйвера

webdriver.Chrome(options=options) — это не «объект Chrome», а дескриптор сессии. За этой строкой, как мы разобрали в прошлом уроке, скрывается запуск процесса chromedriver, запуск процесса Chrome и HTTP-запрос POST /session. Строка небесплатная: она занимает 1–3 секунды и порядка 200–300 МБ памяти. Именно поэтому драйвер создают один раз на тест (а часто и один на весь файл тестов), а не на каждую проверку.

Options — набор ключей запуска браузера. --headless=new прячет окно: тест работает быстрее и, что важнее, вообще способен работать на сервере CI, где нет графической оболочки. Локально при отладке этот флаг обычно комментируют — глазами видно, где тест споткнулся.

Переход на страницу

driver.get(url)блокирующий вызов: он не вернёт управление, пока браузер не сообщит, что страница загружена (по умолчанию — пока document.readyState не станет complete). Это одновременно и удобно, и источник главного заблуждения новичков.

Удобно: не надо ставить sleep после get. Заблуждение: «раз get дождался загрузки, значит на странице уже всё есть». Не значит. readyState == complete говорит лишь о том, что загрузился HTML и подгрузились ресурсы; данные, которые фронтенд дорисует после ответа AJAX-запроса, могут появиться через секунду. Для них нужны явные ожидания — им посвящён отдельный раздел курса. Но конкретно <title> у обычной страницы приходит в исходном HTML, поэтому здесь всё честно работает без ожиданий.

Проверка

driver.title — это GET /session/{id}/title, то есть свойство с сетевым запросом внутри. Обращаться к нему в цикле — плохая идея, сохраните в переменную.

Дальше — сердце теста. Обратите внимание на второй аргумент assert: это сообщение об ошибке, и оно решает, будете ли вы через месяц понимать, что случилось в ночном прогоне. Сравните «AssertionError» без пояснений с текстом ниже. Механику assert можно пощупать прямо здесь, без браузера — это чистый Python:

expected = "Example Domain"

# случай, когда всё хорошо
actual = "Example Domain"
assert actual == expected, f"Ожидали заголовок '{expected}', получили '{actual}'"
print("OK:", actual)

# а теперь сервер отдал ошибку, и заголовок другой
actual = "502 Bad Gateway"
try:
    assert actual == expected, f"Ожидали заголовок '{expected}', получили '{actual}'"
except AssertionError as e:
    print("FAIL:", e)

Результат:

OK: Example Domain
FAIL: Ожидали заголовок 'Example Domain', получили '502 Bad Gateway'

Вторая строка сразу говорит: продукт лежит, тест ни при чём. Ради таких сообщений и пишут второй аргумент.

Про строгость сравнения: == подходит для статичного заголовка. Если в title подмешивается что-то изменчивое («Корзина (3) — Магазин»), берут вхождение: assert "Магазин" in driver.title. Проверять по in «на всякий случай» везде — плохая привычка: тест перестаёт ловить опечатки.

Почему quit() обязателен

driver.quit() шлёт DELETE /session/{id}: драйвер закрывает браузер и завершает сам себя. Если этого не сделать, в системе остаются жить два процесса — Chrome и chromedriver, каждый со своей памятью и своим временным профилем на диске.

На локальной машине после десятка запусков вы это заметите по вентилятору. В CI последствия жёстче: агент собирает сотню «осиротевших» Chrome, упирается в лимит памяти, и падать начинают чужие тесты — те, что после вас. Найти причину такого «плавающего» падения крайне сложно, поэтому дисциплина закрытия сессии не обсуждается.

Два подводных камня:

  • close() — это не quit(). close() закрывает текущую вкладку; если она была последней, браузер закроется, но процесс chromedriver останется. Правило простое: close() — для вкладок, quit() — для завершения работы.
  • quit() должен вызываться при падении теста. Если написать его последней строкой без try/finally, то при упавшем assert исключение улетит вверх — и до quit() выполнение просто не дойдёт. То есть браузер останется висеть ровно в том случае, ради которого мы всё и затевали: когда тест упал.

Отсюда try/finally в примере. Есть и более элегантный вариант — драйвер поддерживает контекстный менеджер, и quit() вызывается на выходе из блока автоматически:

from selenium import webdriver

with webdriver.Chrome() as driver:      # quit() будет вызван сам, в том числе при исключении
    driver.get("https://example.com/")
    assert driver.title == "Example Domain", f"Заголовок: {driver.title}"

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

Как это работает

Полная хронология запуска нашего теста:

Строка кодаЧто происходит на самом деле
webdriver.Chrome(options=...)Selenium Manager находит драйвер в кэше; стартует процесс chromedriver на свободном порту; тот стартует Chrome с чистым временным профилем; уходит POST /session, возвращается sessionId
driver.get(url)POST /session/{id}/url; браузер грузит страницу; драйвер отвечает, только когда document.readyState == "complete"
driver.titleGET /session/{id}/title; драйвер читает document.title и возвращает строку
assert ...чистый Python, никакой сети: сравнили строки, при несовпадении бросили AssertionError
driver.quit()DELETE /session/{id}; Chrome закрывается, временный профиль удаляется, процесс chromedriver завершается

Заметьте: из пяти строк четыре — это сетевые вызовы, и только assert выполняется мгновенно. Такова цена работы с настоящим браузером.

Частые ошибки

  • Скрипт без assert. Открыли страницу, напечатали driver.title, порадовались. Это не тест: он никогда не покраснеет. Тест обязан уметь падать.
  • quit() последней строкой без try/finally. При падении теста браузер остаётся висеть — и виснет ровно тогда, когда это больнее всего.
  • close() вместо quit(). Вкладка закрылась, chromedriver жив, память течёт.
  • time.sleep(5) после get(). Лишнее: get() и так ждёт загрузки. А для динамического контента sleep всё равно не решение — нужны явные ожидания.
  • Драйвер создаётся внутри цикла. Сто итераций — сто запусков браузера, тест идёт полчаса. Создавайте драйвер один раз.
  • Забыли --headless=new в CI. На агенте нет графической оболочки, Chrome не стартует, тест падает с невнятной ошибкой запуска — при том что локально всё зелено.
  • Проверка через driver.page_source. Соблазнительно («ищем подстроку во всём HTML»), но это проверка «текст где-то есть на странице» — он может лежать в скрытом блоке, в комментарии или в JSON внутри <script>. Проверяйте конкретное свойство или конкретный элемент.

Итоги

  • Минимальный тест — четыре шага: webdriver.Chrome()get()assertquit(). Без assert это не тест.
  • get() блокирует выполнение до document.readyState == "complete", поэтому sleep после него не нужен — но и «всё загрузилось» это не гарантирует.
  • Второй аргумент assert — сообщение об ошибке. Пишите его всегда: именно его вы будете читать в 3 часа ночи в логе CI.
  • quit() закрывает и браузер, и процесс драйвера; close() — только вкладку. Вызывать quit() нужно в try/finally или через with, иначе упавший тест оставит зомби-процессы.
  • Создание драйвера — дорогая операция (секунды и сотни мегабайт). Один драйвер на тест, а лучше — на файл, через фикстуру.
Проверьте себя
1. Почему driver.quit() вызывают внутри блока try/finally, а не просто последней строкой теста?
AТак тест выполняется быстрее
BИначе при упавшем assert исключение прервёт тест и quit() не выполнится — браузер и chromedriver останутся висеть
Cfinally заставляет Selenium повторить упавшую проверку
DБез finally Selenium не сможет сделать скриншот страницы
2. Чем driver.close() отличается от driver.quit()?
AЭто полные синонимы
Bclose() закрывает текущую вкладку, а quit() завершает сессию: закрывает браузер и процесс драйвера
Cclose() завершает драйвер, а quit() только сворачивает окно браузера
Dclose() закрывает браузер, но оставляет вкладки открытыми в фоне