Selenium тестирование в Python

| Python

Библиотека Selenium в Python предоставляет простой API для написания функциональных и интеграционных тестов веб-приложений. Благодаря Selenium Python API легко получить доступ ко всем функциям Selenium WebDriver интуитивным способом.

Для работы Selenium требуется WebDriver. В настоящее время есть WebDriver для Firefox, Chrome, Edge и Safari. В примерах далее будет использован chromedriver.

Установка Selenium

Для установки пакета selenium воспользуемся программой pip. Установку рекомендуется проводить в виртуальном окружении Python.

(venv) $ pip install selenium

Установка chromedriver

Процесс установки довольно прост, потому что chromedriver просто исполняемый файл в операционной системе, с которым Selenium будет взаимодействовать в процессе тестирования. Сверьтесь с официальной страницей chromedriver для загрузки и последующей установки свежей версии программы. Установка в Linux выглядит так :

(venv) $ curl -L https://chromedriver.storage.googleapis.com/2.32/chromedriver_linux64.zip -o chromedriver.zip
(venv) $ sudo unzip chromedriver.zip -d /usr/bin/
(venv) $ sudo chmod +x /usr/bin/chromedriver

Управление Chrome из Python

Теперь импортируем пакет webdriver из selenium сказав ему использовать chromedriver.

from selenium import webdriver

driver = webdriver.Chrome()

Выполнение этого кода приведёт к кратковременному появлению окна Chrome. Дальше ничего не будет происходить, потому что пример не сообщает webdriver никаких дополнительных инструкций.

Решим простую задачу получения текущей версии сайта. Беглый взгляд на код главной страницы показывает, что версия находится внутри разметки:

…
    <div class="large-4 medium-4 small-4 columns">
        <div class="callout">
            <span class="date">20 декабря 2017 г.</span>
            <p><a href="news/15">Релиз 1.8.0</a></p>
        </div>
    </div>
…

Таким образом, версия программы находится в тэге a внутри div с классом callout. Поиск его на странице показывает, что этот класс не уникален, поэтому используем Selenium для поиска первого элемента и получения его содержимого следующим образом:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get('http://localhost/')
element = driver.find_elements_by_css_selector('.callout a')[0]
print(element.text)
driver.close()

Запуск кода приведёт к вызову окна Chrome, которое загружает страницу целиком, затем производится поиск нужного результата с последующей его печатью в терминал. После выполнения скрипта производится закрытие окна браузера. В этом примере используется селектор CSS с Selenium методом find_elements_by_css_selector. Для навигации по страницам также доступно множество других методов find_element_by_ *.

Скрытый запуск браузера

Для последующих примеров будет полезно использовать chromedriver в режиме скрытого запуска браузера. Для этого немного изменим предыдущий пример:

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

options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(chrome_options=options)
driver.get('http://localhost/')
element = driver.find_elements_by_css_selector('.callout a')[0]
print(element.text)
driver.close()

Выполнение этого кода распечатает текст текущей версии, как и в предыдущем примере, но на этот раз не появится окно Chrome. Это связано с тем, что новая версия использует класс webdriver.chrome.options для передачи аргументов в бинарник Chrome с использованием chrome_options. Единственная опция которая здесь передается --headless, которая сообщает Chrome о выполнении действий без визуализации.

Разница в скорости выполнения двух примеров незначительна, но при использовании на реальных тестах, с возможными сотнями отдельных тестов, экономия времени будет существенной.

Ключевые возможности selenium

Существует множество важных классов и методов Selenium, которые будут широко использоваться для тестирования веб-страниц. Вот краткий список некоторых ключевых функций, о которых нужно знать. - Поиск элементов - Заполнение input - Очистка input - Ожидание

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

Всего восемь методов поиска (плюс ещё восемь во множественном числе):

find_element_by_class_name
find_element_by_css_selector
find_element_by_id
find_element_by_link_text
find_element_by_name
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_xpath

Все эти методы довольно длинные, поэтому хорошим помощником является класс webdriver.common.By. By может заменить методы более длинной формы простым сокращением. Предыдущий пример кода можно заменить на:

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

driver = webdriver.Chrome()
driver.get('http://localhost/ ')
element = driver.find_elements(By.CSS_SELECTOR, '.callout a')[0]
print(element.text)
driver.close()

Хотя этот код не является кратким, поэтому предлагается его немного доработать создав метод обертки для поиска элементов. Это должно значительно сократить усилия по набору этих методов по мере увеличения размера теста и сложности. Вот примерная обёртка:

def find(self, by, value):
    elements = self.driver.find_elements(by, value)
    if len(elements) is 1:
        return elements[0]
    else:
        return elements

Используется множественный метод find_elements и возвращает либо список, либо один элемент в зависимости от того, сколько их найдено. При этом можно использовать find(By.ID, 'my-id'), вместо driver.find_element_by_id('my-id'). Эта форма преобразует код в гораздо более чистый, особенно при переходе между различными доступными методами поиска.

Отправка Input

Большинство проектов веб-приложений будут иметь дело с полями ввода, Selenium также это хорошо поддерживает. Каждый класс webelement (результат различных методов find_element) содержит метод send_keys, который может использоваться для моделирования ввода текста в элементе. Попробуем использовать эту функцию для поиска «Python» в Википедии.

Быстрый просмотр источника страницы Википедии показывает, что элемент ввода поиска использует идентификатор searchInput.

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

driver = webdriver.Chrome()
driver.get('https://www.wikipedia.org/')
el = driver.find_element(By.ID, 'searchInput')
el.send_keys('Python')

Вышеприведенный код откроет окно Chrome с загруженной страницей в Википедии и «Python» в поле ввода поиска. Это окно остаются открытым, потому что код не включает команду driver.close(), которая используется в предыдущих примерах.

Есть несколько вариантов отправить форму. Далее приведено несколько способов, которые позволяют это выполнить.

Отправка содержимого формы

Еще раз взглянув на html источник в Википедии, форма поиска содержит id=search-form. Далее идентификатор можно использовать с методом webelement.submit() для отправки формы.

Добавьте в предыдущий пример следующий код:

form = driver.find_element(By.ID, 'search-form')
form.submit()

Запуск кода приведёт к открытию окна Chrome с результатами поиска в Википедии слова Python.

Нажатие кнопки submit формы

Страница поиска Wikiedpia включает в себя причудливую, стилизованную кнопку отправки запроса поиска. У него нет уникального идентификатора, поэтому для определения и «щелчка» кнопки код должен использовать какой-либо другой метод. Это единственный элемент button внутри формы поиска, поэтому его можно легко выбрать с помощью селектора CSS.

Добавьте в пример использования Input:

button = driver.find_element(By.CSS_SELECTOR, '#search-form button')
button.click()

Нажатие клавиши ENTER

В заключении, Selenium содержит набор кодов клавиш, которые могут использоваться для имитации нажатия «специальных» (не буквенно-цифровых) клавиш. Эти коды находятся в webdriver.common.keys. Чтобы отправить форму, код должен будет использовать клавишу Enter, поэтому пересмотренная версия поискового кода Википедии выглядит так:

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

driver = webdriver.Chrome()
driver.get('https://www.wikipedia.org/')
el = driver.find_element(By.ID, 'searchInput')
el.send_keys('Python')
el.send_keys(Keys.RETURN)

Как и два предыдущих примера, этот скрипт запустит открытую страницу Chrome с результатами поиска в Википедии.

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

Очистка input

Хотя Selenium предлагает метод webelement.clear(), его реализация несовместима в разных браузерах, и ее поведение может быть определено по-разному в зависимости от тестируемого приложения и элемента. По этим причинам не рекомендуется им пользоваться для очистки полей ввода формы. Вместо этого класс Keys может использоваться для имитации нажатия клавиши backspace несколько раз в input.

Простая функция реализации.

from selenium.webdriver.common.keys import Keys


def clear(element):
    value = element.get_attribute('value')
    if len(value) > 0:
        for char in value:
            element.send_keys(Keys.BACK_SPACE)

Простая функция будет принимать WebElement, вычислять длину его атрибута value и имитировать нажатие BACK_SPACE, пока весь текст не будет удален из input.

Давайте используем Selenium для поиска в Google слова «selenium». Элемент input поиска Google не имеет уникального идентификатора или класса, но он использует атрибут name со значением «q». Его можно использовать для поиска элемента и отправки keys.

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


def clear(element):
    value = element.get_attribute('value')
    if len(value) > 0:
        for char in value:
            element.send_keys(Keys.BACK_SPACE)

driver = webdriver.Chrome()
driver.get('https://www.google.com/')
el = driver.find_element(By.NAME, 'q')
el.send_keys('selenium')
el.send_keys(Keys.RETURN)

На странице результатов поле поиска по-прежнему имеет значение «q» и теперь заполнено значением «selenium». Хотя имя не изменилось, Selenium нужно будет снова найти элемент, потому что страница изменилась. Добавьте в код изменения, чтобы найти элемент и использовать пользовательскую функцию clear() для его очистить:

el = driver.find_element(By.NAME, 'q')
clear(el)

В целом BACK_SPACE должен быть намного надежнее, чем метод webelement.clear().

Ожидание

«Ожидание» в Selenium может быть обманчиво сложной проблемой. До сих пор все примеры основывались на способности Selenium дождаться, когда страница закончит загрузку, прежде чем предпринимать какие-либо конкретные действия. Для простых тестов это может быть вполне достаточным. Но по мере усложнения тестов и приложений этот метод может не всегда быть пригодным.

Selenium предоставляет некоторые полезные инструменты для решения этой проблемы -

Неявные ожидания Самый простой способ добавить вызов метода WebDriver.implicitly_wait (). Метод принимает целочисленный ввод, который определяет, сколько секунд ждать при выполнении любого из методов find_element.

По умолчанию неявное ожидание равно нулю (или нет ожидания), поэтому, если конкретный элемент не найден сразу, Selenium будет вызывать исключение NoSuchElementException. Попробуем найти элемент name с атрибутом «query» на GitHub (его нет):

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

driver = webdriver.Chrome()
driver.get('https://www.github.com/')
el = driver.find_element(By.NAME, 'query')

Этот код вызовет исключение NoSuchElementException после того, как Chrome загрузит домашнюю страницу GitHub.

Теперь давайте проверим код ниже, который устанавливает неявное время ожидания в пять секунд для одной и той же невыполнимой задачи:

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

driver = webdriver.Chrome()
driver.implicitly_wait(5)
driver.get('https://www.github.com/')
el = driver.find_element(By.NAME, 'query')

Этот код создаст то же самое исключение, но он будет ждать пять секунд, прежде чем это сделать.

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

Рекомендуется всем тестам установку 10 секундного неявного времени ожидания. Это должно помочь предотвратить прерывистые исключения, вызванные проблемами с такими базовыми элементами, как сетевое подключение или баги веб-серверов.

Ожидаемые условия (явные ожидания)

Когда неявных ожиданий недостаточно, ожидаемые условия чрезвычайно ценны. Класс WebDriverWait предоставляет методы until() и until_not(), которые могут использоваться с ожидаемыми условиями для создания более сложных и нужных условий ожидания.

presence_of_element_located() примет объект, описывающий метод и локатор, и вернет true, если объект существует в DOM. Это можно использовать с WebdriverWait.until () и временем ожидания (в секундах):

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebdriverWait
from selenium.webdriver.support import expected_conditions as ec

driver = webdriver.Chrome()
WebdriverWait(driver, 5).until(ec.presence_of_element_located((By.ID, 'html-id')))

Для чего нужен WebDriverWait? Кратко - одностраничные приложения (SPA). Тестирование может потребовать перемещения навигационных элементов приложения, и если страница не будет полностью перезагружена, Selenium должен будет использовать WebDriverWait, чтобы делать такие вещи, как ожидание загрузки нового раздела или таблицы данных после вызова API в стиле AJAX.

Другие ожидаемые условия будут в значительной степени соответствовать одному и тому же синтаксису и в основном иметь (очень) подробные имена. Два из других, которые я нашел полезными на практике, - text_to_be_present_in_element() и element_to_be_clickable().

Время ожидания

Также используется метод обходного пути для простого, явного ожидания по времени без каких-либо ожидаемых условий. Одна из областей, где это может пригодиться - это тестирование результата «секундомера» на Javascript, который обновляется в реальном времени. В рамках теста я запускаю секундомер, подождите две секунды, а затем проверю, чтобы отображаемое время было правильным. Чтобы достичь этого, я создал метод, который по существу делает ожидаемое условное ожидание, которое умышленно уходит в прошлое:

from selenium import webdriver
from selenium.webdriver.support.ui import WebdriverWait

def wait(self, seconds):
    try:
        WebdriverWait(self.driver, seconds).until(lambda driver: 1 == 0)
    except TimeoutException:
        pass

Этот метод можно использовать, например, ждать пять секунд, вызвав wait (5). WebDriverWait вызовет исключение через пять секунд, потому что аргумент until () - простая лямбда, которая всегда будет возвращать False. Улавливая и передавая исключение, метод просто ожидаеи указанное количество секунд и ничего больше.