не так страшен xpath как его незнание
Вот почему мы всегда пишем селекторы на XPath
Привет, Хабр! Сегодня хочется поговорить про XPath — мощный и гибкий инструмент для работы с веб-интерфейсами, который при этом почему-то остается не особенно популярным. Статей и мануалов по XPath очень много, и в этом посте я постараюсь рассказать, как мы применяем данный инструмент и почему считаем его более эффективным, чем другие подходы. Если вам знаком термин “селектор”, а тем более — если вы слышали про XPath, добро пожаловать под кат, там много полезного!
В нашей команде работает целая группа инженеров-тестировщиков, которые ежедневно пишут автоматические end2end тесты для Selenium, а также создают огромное количество селекторов для них. С одной стороны, эта работа кажется несложной, но на практике к ней добавляются условия:
Писать код надо понятно и однотипно, потому что вы — не единственный инженер;
Всегда надо предусматривать возможность оперативно переписать селектор;
Для целого множества сайтов и версток необходимо обеспечить единый подход;
Важно гарантировать однозначность каждого селектора;
И, наконец, каждый селектор должен быть максимальную информативным.
С учетом всех этих требований, работа инженера-тестировщика становится не такой уж простой, потому что выполнять задачи необходимо с определенным уровнем унификации. И именно поэтому мы полностью отдали предпочтение XPath (XML Path Language) для написания селекторов.
Существует мнение (и оно довольно распространенное), что XPath это что-то громоздкое, со сложным синтаксисом. А некоторые также ставят под сомнение скорость поиска по XPath. Например, в одном из популярных курсов обучения по автоматизации тестирования с помощью Selenium, вы можете увидеть вот такие мысли:
Но наша практика показывает, что это не совсем так…а может быть даже совсем не так. Мы сделали ставку на XPath, потому что наша команда пишет автоматизированные тесты для заказчиков — фактически тысячи тестов. Чтобы увязать их с внедрением систем комплексного мониторинга, об этом я уже писал в прошлом посте. При таких объемах в условиях командной работы, стандартизация подходов является необходимостью, в том числе и для составления селекторов.
XPath: плюсы и минусы
Начнем с минусов — то есть с того, почему XPath не любят.
Минус №1. Холивары о скорости работы селекторов на XPath и, например, CSS действительно не затихают. Мы ввязываться в них не будем и не станем утверждать, что тот или иной подход работает быстрее. Но при этом стоит отметить, что, учитывая общее время выполнения UI теста, разница в скорости работы селектора вообще не существенна.
Минус №2. Многие считают селекторы XPath неинформативными. И это мнение обычно формируется после работы с плагинами для браузеров и стандартными средствами браузеров по поиску XPath. Действительно, селектор вида //div[1]/div[2]/ul/li/a не вызывает оптимизма. И мы, кстати, рекомендуем, не пользоваться подобными инструментами.
Минус №3. При всей мощности XPath остаются вопросы, которые он не может решить. Например, на XPath не получится сделать селекторы к псевдоклассам, и содержимому Shadow DOM. Но, как говорится, каждому инструменту — своя сфера применения.
С плюсами XPath все гораздо проще, ведь они очевидны и лежат на поверхности:
Плюс №1. Возможность поиска по тексту элемента. Первое, что мы встречаем в любом web-приложении — это текст. Текст в том числе располагается на кнопках, ссылках, выпадающих меню. И если свойства таких элементов могут измениться, то текст чаще всего останется прежним. Таким образом, даже изменения верстки никак не повлияют на XPath-селекторы.
Как следствие, через XPath можно искать элементы с изменяемым текстом. Например, если нужно выбрать в календаре “позавчера”, можно прописать дату в явном виде в селекторе XPath. Также благодаря этой функции появляется возможность создавать селекторы для сложных таблиц. Например, это будет полезно, если вам необходимо выбрать ячейку в некой строке, причем строку и столбец можно найти только по тексту, так как номера строки и столбца могут меняться.
Все это очень полезно, когда речь заходит о реальной практике. Вот 3 примера, в которых преимущества XPath видны, так сказать, невооруженным взглядом:
Пример №1. Селектор авторских постов в блоге
Берем первый попавшийся html код.
Давайте напишем селектор для
Да, такой селектор будет жить…но вполне возможно, что тоже не долго.
Тем временем, на XPath селектор может выглядеть следующим образом:
//[normalize-space(.)=’Ходили купаться’ and contains(@class, ‘title’)]
Выглядит громоздко. Но сколько плюсов:
Мы привязали селектор к самому тексту заголовка, который никто не вправе изменять, кроме автора.
normalize-space(.) исключает проблему случайных/лишних пробелов.
Верстальщик может без последствий добавлять/изменять список применяемых классов — селектор останется рабочим.
И последняя, но очень важная возможность: Представьте, что перед вами задача не написать тест и селекторы, а отредактировать селектор в существующем тесте. на проде. срочно, а ещё лучше вчера…
Что проще найти и изменить, например в тысяче строк кода?
Для нас очевиден Вариант №2.
Пример №2. Поиск по DOM-дереву
Возможность поиска элементов по DOM-дереву вниз или вверх позволяет использовать XPath, чтобы добраться до самых глубоко спрятанных элементов страницы.
Возьмем реальный код
Посмотрите как изящно и однозначно выглядят селекторы XPath в таком случае:
Здесь мы оттолкнулись от существующего поля name, ушли вверх по дереву, а затем вернулись к нужным тегам. Замените значение поля name и название опции на переменные и селекторы будут универсальны для любого из меню на странице.
Пример №3. XPath справляется там, где другие не справляются
Таблицы довольно часто встречаются на web страницах. При этом нередко речь заходит о множестве таблиц на одной странице, а каждая из них может содержать сотни строк.
Ниже мы приводим только часть (упрощенного) кода.
Как сделать селектор к ссылке ячейки “На согласовании”? Классы остались где-то наверху, теги все одинаковые, атрибутов нет от слова “совсем”…XPath тут справляется “на ура”, благодаря своим функциям и полнотекстовому поиску:
Такой селектор легко читается, а значит в него легко внести правки, если это необходимо.
Кстати, тут XPath демонстрирует дополнительную гибкость. Если в примере выше “На согласовании” будет целое множество “Закупок”, то мы сможем добавить номер закупки как ещё одно условие. Например, вот так:
//*[contains(.,’Сделки’)]//tr[contains(.,’Закупка 4′)]//td[contains(.,’На согласовании’)]//a
Итого: почему именно XPath?
Фактически XPath похож на язык программирования: хороший XPath-селектор легко читаем, по нему сразу ясно, о каком элементе идет речь. Такое положение дел добавляет удобства в работе с XPath, увеличивает скорость выполнения типовых задач и, как следствие, сокращает время разработки.
К тому же XPath позволяет осуществлять поиск вообще по любому атрибуту элемента. Разработчики зачастую добавляют свои атрибуты ко множеству тегов. Через CSS и стандартными методами фреймворков тестирования их не найти. XPath здесь тоже выручает, например, вот так можно сделать селектор по кастомному атрибуту:
Подводя итог, скажу, что мы выбрали для себя XPath как наиболее удобное средство для создания селекторов и рады поделиться своим опытом как с заказчиками, так и с коллегами “по цеху”. Но не любой селектор, написанный на XPath однозначно хорош. В следующем посте я подробно расскажу о “плохих” и “хороших” практиках использования XPath, которые мы определили, набивая свои собственные шишки. А сейчас прошу всех заинтересованных поучаствовать в нашем опросе.
xpath
XPATH, вверх и вниз по DOMу
Итак, мы посмотрели как писать локаторы XPATH, как писать CSS, договорились не копировать локаторы слепо и по возможности писать кратко и понятно.
Напоминаю алгоритм написания любого локатора:
1) Если есть, то используем уникальный id или имя класса
2) Если не выполняется пункт 1, то идем к разработчикам и просим его обеспечить
3) Если не выполняется пункт 2, то пытаемся использовать CSS, если можно написать коротко
4) Keep calm and use XPATH
Сегодня я покажу написание локаторов на небольшом и конечно искусственном примере, который показывает, что не всегда можно использовать CSS или другие локаторы. Перед нами сайт дорого во всех смыслах Ростелеком, с предложениями по интернет: https://spb.rt.ru/packages/tariffs
В тестах нам нужно нажать кнопку «Подключить» у тарифа с ценой 1010 рублей. Но мы при этом знаем, что тарифы часто меняются и данный тариф с его кнопкой может быть расположен в любом месте страницы, с любым id. (На самом деле задача реальная, но любые совпадения случайны)
Исходя из условий, мы не можем просто получить список кнопок с надписью «Подключить» и кликнуть вторую, в любой момент она может стать третьей. Мы также не можем быть уверены, что id тарифа не изменится, кроме того, было бы круто иметь один общий локатор, который бы нашел кнопку «Подключить» у тарифа по его цене. Класс также не уникален и возвращает список элементов.
Что же есть уникальное, к чему можно прицепиться? Цена тарифа! У всех тарифов разные цены, причем в данном случае мы как раз ищем цену 1010 рублей.
У элемента цены есть уникальный атрибут data-fee=’1010′ по которому мы можем точно найти его, причем только его одного.
//*[@data-fee=’1010′] — мы точно находим нужную цену. Теперь нужно двигаться к кнопке Подключить по дереву DOM и тут есть два варианта:
1) подняться от цены к ее родителю ( class =» tariff-desc__cost_m-cell «), от него получить следующий сестринский элемент ( class =» tariff-desc__cost_l-cell «) и уже в нем получить кнопку. Получается один шаг вверх и два вниз
//*[@data-fee=’1010′]/.. — переходим к родителю
//*[@data-fee=’1010′]/../following-sibling::div — уходим вниз к сестринскому элементу
//*[@data-fee=’1010′]/../following-sibling::div/a — получаем нашу кнопку
//*[@data-fee=’1010′]/ancestor::div[contains(@class,’js-price’)] — получаем предка(ancestor), с классом содержащим js-price
//*[@data-fee=’1010′]/ancestor::div[contains(@class,’js-price’)]//a — у нашего элемента просто ищем вложенную ссылку (нашу кнопку).
Можете убедиться, что оба локатора являются уникальными, и меняя цену, можно получать локаторы кнопки у всех остальных тарифов по их цене.
Надеюсь вам никогда не придется идти в алгоритме поиска элемента дальше 1 пункта.
Не xpath единым, или использование CSS
Да, я в курсе, что в сети полно блогов и туториалов по использованию CSS, но у них всех есть общая проблема — они стремятся рассказать о всех функциях и вариантах использования и не говорят когда именно использовать именно CSS локаторы. В итоге начинающий не знает за что схватиться и не понимает когда использовать CSS, а когда Xpath.
По своему опыту убежден, что все функции не нужны для постоянной работы в 99% случаев ни в XPATH, ни в CSS. Основные полезные функции xpath я уже описывал, теперь обсудим Css и когда какие локаторы использовать.
div class =» ex-cnt « > div >
Чтобы получить потомка любой вложенности (аналог // или descendant в xpath) нужно просто поставить пробел! Чтобы найти элемент с классом «ex-cnt» в примере выше, используем div. signInUsingEmailLbl div.ex-cnt то есть опять же находим родителя, а потом любого потомка с классом ex-cnt.
На мой взгляд — это самые полезные функции в CSS, которые легко запомнить, короткие в написании и понимании. По поводу скорости CSS по сравнению с Xpath я уже говорил — разница незначительна, но конечно чем короче в написании локатор, тем лучше. Минусом СSS является то, что поиск идет только сверху вниз, то есть мы не можем найти предка, только потомков (вспоминаем в xpath функции ancestor и parent).
Итак, используем CSS если:
В других случаях, когда нам нужен предок, нужно движение по иерархии вверх (а иногда вверх-вниз!), нужны сложные условия для получения элемента, лучше использовать xpath. Про логику и правила нахождения локаторов в сложных случаях верстки напишу в другой статье.
Не так страшен XPATH как его незнание
Сначала о том, почему новички (и не только) не любят xpath:
А теперь о том, как обстоят дела на самом деле и в чем преимущества xpath, если его правильно использовать:
— он не уступает (или незначительно уступает) в скорости css
— он понятен и легко читаем, по нему можно понять о каком элементе идет речь
— он похож на язык программирования и его удобно использовать
— можно добраться до самых запрятанных элементов страницы, благодаря выстроенным цепочкам отношений
Итак, несколько правил использования xpath:
Переходим к делу и практике, тот xpath, что указан выше (//header/div/ul/li[2]/a) на самом деле можно указать в виде //a[text()=’Pricing’]. Согласись, что есть разница и в длине текста и в понимании его, ведь тут видно по тегу, что это ссылка и ее текст –Pricing. То есть ты можешь и сам найти этот элемент на странице визуально и в случае исключения с таким локатором сразу знаешь, что и где искать!
Теперь о тех командах, которые тебе реально пригодятся для написания грамотных и удобных локаторов:
Как видим id явно сгенерирован и привязаться к нему нельзя, класс тоже не внушает доверия, кроме того Selenium не разрешает использовать сложносоставные имена в локаторе className, но тут есть текст, который решает проблему: //a[text()=’Contact us’]
Кроме того, очень полезная возможность – это искать элемент по одному из слов в названии класса.Пример:
Теперь пойдут команды отношения элементов (предок, родитель, ребенок, потомок, сестринский элемент), которые позволяют очень гибко найти практически любой элемент на странице при грамотном применении.
Формат использования //начальный элемент/отношение::тег(фильтр) конечного элемента. Обрати внимание на два двоеточия после отношения и не забывай после двоеточий указать тег, а лучше и фильтр искомого элемента, так как потомков может быть и много, а нам нужен какой-то конкретный.
Нам нужно ввести текст в input, но как видишь тут имеется ряд проблем – id динамический, классов и сгенеренных id со словом input на странице много, привязаться вроде не к чему. Но тут есть элемент с текстом, который уникален для страницы, вот к нему и прицепимся:
//div[text()=’Тема’]/preceding-sibling::input — мы сначала находим уникальный элемент с текстом, а потом от него ищем предшествующий сестринский элемент, делая фильтр-уточнение, что ищем именно input. Еще пример:
Нам нужно кликнуть кнопку, на которой нет текста, только иконка, но как видишь у нее все те же проблемы с id плюс есть куча одноименных классов. Нас спасает то, что у предшествующего элемента есть уникальное название класса, вот от него и будем плясать: //div[contains(@class,’listViewMoreActionsButton’)]/following-sibling::div – находим элемент у которого есть уникальное слово в названии класса и от него уже ищем следующий сестринский элемент, с тегом div. Учитывай, что даже если сестринских последующих элементов с тегом div будет много вернется только самый первый!
То представим, что нам нужен непосредственно элемент с для всех на данной картинке он является родителем (parent) и поэтому его можно вытянуть через любой из них, например //div[text()=’Тема’]/parent::div
Кстати, обращение к родительскому элементу, можно заменить двумя точками и без тега, вот так //div[text()=’Тема’]/..
Так как все элементы в примере — дети, то можно любого из них найти от родителя вот так:
//div[contains(@class,’has-floating’)]/child::input – находим родителя, а от него ищем ребенка с тегом input.
Нам нужна папка именно с определенным именем, но верстка организована так, что сам текст не содержится именно в элементе класса папка, поэтому нам надо найти сначала класс, а потом отфильтровать ту, у которой в потомках есть нужный текст:
//div[@class=’listitem Folder’]/descendant::span[text()=’Folder name’] –сначала находим класс папки, потом среди его потомков ищем тег span и нужный нам текст. Вы можете спросить –а почему просто по тексту не искать? Дело в том, что элементов с таким текстом на странице может быть больше одного, а нам нужна именно папка.
//div[@class=’listitem Folder’]//span[text()=’Folder name’]
Важно понимать, что можно, но крайне нежелательно использовать в одном локаторе несколько отношений, например:
//div[@class=’One]/ child ::div[@class=’Two’]/ descendant ::input[@class=’Three]. Такой локатор работать будет, но он уже сложно читается и скорее всего есть возможность подобрать другой, не нужно использовать такое без необходимости, помним правило номер 2. Совсем недопустимо использовать в одном локаторе обратные отношения то есть сначала искать потомка, потом его предка или наоборот.
Это все команды и отношения, которые вам пригодятся при написании локаторов! Да, есть еще и другие, вы можете с ними ознакомиться в мануале, прикрепленном в начале статьи, однако я их практически не использовал, а указанных в статье хватает мне и по сей день.
Итак, применяйте указанные команды, ищите правильные элементы, соблюдайте вышеозначенные правила и у вас не будет проблем с написанием грамотных локаторов, которые легко понять, прочесть, исправить. А главное вы поймете, что xpath очень удобен для написания локаторов к любым элементам.
Конструирующий XPath? Алгоритмический XPath? Ничего, кроме XPath
Итак, обычный XPath описывает последовательность шагов продвижения по дереву документа, причем на каждый шаг может быть наложен фильтр-условие (предикат, записываемый в квадратных скобках). В результате получаем какое-то конечное множество узлов или число или строку или логическое значение. Нас интересует, прежде всего, случай множества узлов. Обычный XPath выдает узлы, которые уже существуют в документе. Рассмотрим гипотетический конструирующий XPath, который будет не только возвращать уже имеющиеся узлы, но еще и достраивать новые узлы таким образом, чтобы они полностью соответствовали запросу.
Идея очень проста – на очередном шаге XPath-запроса будем анализировать фильтр-предикат и составлять варианты данных, которые под этот фильтр подпадают. А потом будем проверять, какие из этих вариантов уже существуют и достраивать несуществующие.
Например, вот запрос:
Итак, все просто. В результате применения такого конструирующего XPath, например, к документу
получим выходной документ:
И при этом мы обошлись исключительно средствами XPath, без XSL или еще чего-либо в этом роде.
Алгоритмический XPath
Итак, мы научили XPath создавать данные. Теперь поучим его (немного) их алгоритмически обрабатывать.
Последовательность операторов можно описать обычным логическим AND-выражением. Оно вычисляется строго слева направо, это и есть то, что нужно. Если оно должно быть выполнено полностью, то надо просто позаботиться, чтобы все его элементы возвращали истинное выражение.
Условный оператор вида if(A) then B else С, разумеется (и здесь я ничего нового не скажу), можно описать логическим выражением
Все немного сложнее с циклом. Просто так его вводить не очень хотелось, поэтому я решил просто ввести понятие XPath-функции, которая может быть рекурсивной. И тогда можно любой цикл представить цепочкой рекурсивных вызовов с проверкой условия окончания.
В-принципе, это почти все (в минимальном варианте). Не требуется даже переменных – их заменяют элементы текущего документа. Требуются только именованные аргументы функций.
и добавить к ним вызывающий XPath:
Я надеюсь, что вам, уважаемые читатели, будет немного интересно разобраться в таком «коде». Единственное, что я обязательно упомяну – create(XPATH) – это системная функция, которая исполняет свой аргумент XPATH в конструирующем режиме.
А теперь о том, что все это, конечно, интересно, но программировать так, без переменных, все- таки довольно сложно. Понимая это, я ввел полноценные переменные, которые, собственно, в XPath уже есть – они начинаются со знака «$», но я добавил возможность присваивания им значения новой функцией set. Пожалуйста, вот пример функции depth_list с двумя аргументами – ссылкой на начальный элемент, содержащий вложенный список из элементов (как в примере выше), и выходной переменной, в которую помещается длина списка:
Заключение
В появившийся в результате микроязык, который я прозвал XPath Defender, я добавил еще некоторые необходимые функции и воспользовался им в своей системе распознавания и порождения программ PGEN++ для выполнения такой важной задачи, как автоматическое достраивание модели программы, представленной в виде XML-документа. Иными словами, если есть текстовое описание некоторой задачи (для определенности — на русском языке), для решения которой надо сгенерировать программу, то это описание распознается и превращается в упорядоченный набор элементов постановки задачи (объектов с параметрами). Это первичная постановка, которая еще не содержит плана решения задачи. Распознанные элементы помещаются в XML-документ и к ним применяются правила, записанные как в виде простых ограничивающих или порождающих XPath-утверждений, так и в виде фрагментов на XPath Defender (это один из вариантов рабочего процесса). Эти правила проверяют и дополняют XML-документ-модель элементами плана решения. И уже потом по полученной модели система строит решающую программу. Эта схема успешно опробована на простых задачах математической обработки векторных данных.
Но все-таки, самым важным результатом, думаю, является тот факт, что удалось доказать, что можно построить алгоритмический язык программирования, используя почти исключительно средства обычного и конструирующего XPath, пришлось ввести только функции.