Первый тест: открыть страницу и проверить заголовок
Пишем первый настоящий автотест: открыть страницу, проверить заголовок, корректно закрыть браузер — и разбираем каждую строчку, включая ту, которую чаще всего забывают.
Автотест = подготовка (открыли браузер) + действие (перешли на страницу) + проверка (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.title | GET /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()→assert→quit(). Безassertэто не тест. get()блокирует выполнение доdocument.readyState == "complete", поэтомуsleepпосле него не нужен — но и «всё загрузилось» это не гарантирует.- Второй аргумент
assert— сообщение об ошибке. Пишите его всегда: именно его вы будете читать в 3 часа ночи в логе CI. quit()закрывает и браузер, и процесс драйвера;close()— только вкладку. Вызыватьquit()нужно вtry/finallyили черезwith, иначе упавший тест оставит зомби-процессы.- Создание драйвера — дорогая операция (секунды и сотни мегабайт). Один драйвер на тест, а лучше — на файл, через фикстуру.