Привет, Хабр! Сегодня хочется поговорить про 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. Также благодаря этой функции появляется возможность создавать селекторы для сложных таблиц. Например, это будет полезно, если вам необходимо выбрать ячейку в некой строке, причем строку и столбец можно найти только по тексту, так как номера строки и столбца могут меняться. 

Плюс №2. Наличие встроенных функций, таких как contains(), starts-with(), sibling,  normalize-space() и прочих в совокупности с логическим операторами and, not , or позволяет создавать гибкие и универсальные локаторы.

Все это очень полезно, когда речь заходит о реальной практике. Вот 3 примера, в которых преимущества XPath видны, так сказать, невооруженным взглядом:

Пример №1. Селектор авторских постов в блоге

Берем первый попавшийся html код.

<div id="posts" class="post-list">
  <div id="post1" class="item">
    <div class="title">Как я провел лето</div>
    <img src="./images/summer.png">
  </div>
  <div id="post2" class="item">
    <div class="title second">Ходили купаться</div>
    <img src="./images/bad_dog.jpg">
  </div>
  <div id="post3" class="item">
    <div class="title">С друзьями</div>
    <img src="./images/friends.jpg">
  </div>
</div>

Давайте напишем селектор для  <div class="title second">Ходили купаться</div>

Если делать это через CSS, то селектор будет выглядеть так: .second

Но обратите внимание, что у post1 и post3, нет классов first и third! Чем руководствуются разработчики подобной верстки и какие у них мотивы — мы не узнаем никогда, но с определенной долей вероятности можем утверждать, что скоро класса second тоже не будет и значит наши тесты упадут.

Сохраняя приверженность CSS, можно переписать селектор на: #post2 .title

Да, такой селектор будет жить…но вполне возможно, что тоже не долго.

Мы видим из кода, что речь идет о постах, авторских статьях или блоге. Но что будет, когда автор добавит очередной текст? Ведь логично, что новый пост должен быть первым, и тогда нумерация сдвинется, наш селектор #post2 .title будет ссылаться на пост с заголовком Как я провел лето. А неправильный селектор ещё хуже, чем нерабочий селектор, потому что узнаем мы о неправильном селекторе не сразу, если вообще узнаем.

Тем временем, на XPath селектор может выглядеть следующим образом:

//[normalize-space(.)='Ходили купаться' and contains(@class, 'title')]

Выглядит громоздко. Но сколько плюсов: 

  1. Мы привязали селектор к самому тексту заголовка, который никто не вправе изменять, кроме автора.

  2. normalize-space(.) исключает проблему случайных/лишних пробелов.

  3. Верстальщик может без последствий добавлять/изменять список применяемых классов —  селектор останется рабочим.

И последняя, но очень важная возможность: Представьте, что перед вами задача не написать тест и селекторы, а отредактировать селектор в существующем тесте..., на проде..., срочно, а ещё лучше вчера…

Что проще найти и изменить, например в тысяче строк кода?

Вариант 1: 

Вариант 2:

Для нас очевиден Вариант №2.

Пример №2. Поиск по DOM-дереву

Возможность поиска элементов по DOM-дереву вниз или вверх позволяет использовать XPath, чтобы добраться до самых глубоко спрятанных элементов страницы.

Возьмем реальный код

<div class="holder  col-md-8 col-sm-7 col-xs-12 ">
<select data-placeholder="Выбрать..." class="form-control chosen master-field" name="field[new_object_assignment_key]" data-error-message="Поле заполнено некорректно" required="required" style="display: none;">
<option value="">Выбрать...</option>
<option value="006002010">«Тропа» здоровья</option>
</select>
  	<div class="chosen-container chosen-container-single chosen-container-active loaded chosen-with-drop" title="" style="width: 505px;">
<a class="chosen-single chosen-default">
<span style="max-width: 390px;">Выбрать...</span>
<div><b></b></div>
</a>
<div class="chosen-drop">
<div class="chosen-search">
<input class="chosen-search-input" type="text" autocomplete="off">
</div>
<ul class="chosen-results" tabindex="5000" style="overflow: hidden; outline: none;">
<li class="active-result result-selected highlighted" data-option-array-index="0">Выбрать...</li>
<li class="active-result" data-option-array-index="1">«Тропа» здоровья</li>            
</ul>
</div>
</div>

Это пример выпадающего меню на странице. Сначала нам надо кликнуть по ссылке (тег <a>), потом заполнить опцию (тег <input>), а затем выбрать ее (тег <li>). Таких меню на странице может быть несколько.

Посмотрите как изящно и однозначно выглядят селекторы XPath в таком случае: 

Клик по меню: 

//select[@name=’field[new_object_assignment_key]’]/..//a

Ввод опции: 

//select[@name=’field[new_object_assignment_key]’]/..//input

Выбор опции:

//select[@name='field[new_object_assignment_key]']/..//li[normalize-space(.)='«Тропа» здоровья']

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

Пример №3. XPath справляется там, где другие не справляются

Таблицы довольно часто встречаются на web страницах. При этом нередко речь заходит о множестве таблиц на одной странице, а каждая из них может содержать сотни строк.

Ниже мы приводим только часть (упрощенного) кода. 

  <table>
	<tr>...</tr>
	<tr>...</tr>
      <tr><td>Конкурсы</td></tr>
      <tr>
        <td>#1</td>        
        <td>Закупка 15</td>
        <td>Описание
          <a href="">ссылка</a>
        </td>
        <td>Примечания
          <a href="">ссылка</a>
        </td>
      </tr>
      <tr><td>Сделки</td></tr>
      <tr>
        <td>#3</td>
        <td>Закупка 4</td>
        <td>На согласовании
          <a href="">ссылка</a>
        </td>
        <td>Примечания
          <a href="">ссылка</a>
        </td>
      </tr>
	<tr>...</tr>
	<tr>...</tr>	
	<tr>...</tr>      
    </table>

Как сделать селектор к ссылке ячейки “На согласовании”? Классы остались где-то наверху, теги все одинаковые, атрибутов нет от слова “совсем”…XPath тут справляется “на ура”, благодаря своим функциям и полнотекстовому поиску: 

//[contains(.,'Сделки')]//td[contains(.,'На согласовании')]//a

Такой селектор легко читается, а значит в него легко внести правки, если это необходимо.

Кстати, тут XPath демонстрирует дополнительную гибкость. Если в примере выше “На согласовании” будет целое множество “Закупок”, то мы сможем добавить номер закупки как ещё одно условие. Например, вот так: 

//*[contains(.,'Сделки')]//tr[contains(.,'Закупка 4')]//td[contains(.,'На согласовании')]//a

Итого: почему именно XPath?

Фактически XPath похож на язык программирования: хороший XPath-селектор легко читаем, по нему сразу ясно, о каком элементе идет речь.  Такое положение дел добавляет удобства в работе с XPath, увеличивает скорость выполнения типовых задач и, как следствие, сокращает время разработки.

К тому же XPath позволяет осуществлять поиск вообще по любому атрибуту элемента. Разработчики зачастую добавляют свои атрибуты ко множеству тегов. Через CSS и стандартными методами фреймворков тестирования их не найти. XPath здесь тоже выручает, например, вот так можно сделать селектор по кастомному атрибуту:

//input[@data-input-type='SNILS’]

Подводя итог, скажу, что мы выбрали для себя XPath как наиболее удобное средство для создания селекторов и рады поделиться своим опытом как с заказчиками, так и с коллегами “по цеху”. Но не любой селектор, написанный на XPath однозначно хорош. В следующем посте я подробно расскажу о “плохих” и “хороших” практиках использования XPath, которые мы определили, набивая свои собственные шишки. А сейчас прошу всех заинтересованных поучаствовать в нашем опросе.

Комментарии (21)


  1. artem_larin
    18.10.2021 11:33
    +1

    А матчить по рэгэкспу этот ваш xpath может?


    1. SaM1808
      18.10.2021 13:06
      +2

      1. DeniSix
        18.10.2021 14:55
        +1

        Стоит помнить, что дальше XPath 1.0 браузеры не двинулись и поддержку более поздних версий можно получить только с помощью сторонних JS библиотек.


    1. RadST Автор
      18.10.2021 15:21

      Регулярки поддерживаются в XPath версии 2.0. Мы используем Селениум 3, он не умеет XPath 2.0. Но есть обходные пути, в интернете их можно найти, как например по ссылке, что дал SaM1808. И верно подметил DeniSix, браузеры не поддерживают XPath 2.0.


      1. tundrawolf_kiba
        18.10.2021 15:31
        +1

        Мы используем Селениум 3, он не умеет XPath 2.0.

        Не селениум, а браузеры.


  1. AlexeyALV
    18.10.2021 14:13

    А нет ли вероятности,что небольшая переверстка тестируемой страницы при использовании XPath приведет у падению теста? И переделке автотестов?

    В этом плане как раз name, id или кастомные идентификаторы элементов спасут от лишних хлопот.


    1. VanKrock
      18.10.2021 14:43
      +2

      Обычно не влияет, в xpath есть "//" и "/" если вёрстка меняется настолько, что xpath становится не верным, то и css так же поломается, id конечно нужны, но по ним можно искать и используя xpath, а вот поиск по тексту - это удобно для тестов


    1. SaM1808
      18.10.2021 15:59
      +1

      Тут автор же не запрещает их пользовать - пользуйте наздоровье, XPath это умеет, просто имейте в виду ту самую, возможную переверстку... Когда фронтэндер может поменять id или name, а вот текст ему менять просто нельзя.

      Привязаться к id или name - абсолютно нормальная практика IMHO, когда это применимо.


      1. tundrawolf_kiba
        19.10.2021 03:30
        +1

        Привязаться к id или name — абсолютно нормальная практика IMHO, когда это применимо.

        Ну на самом деле лучше сразу готовиться к тому, что текст может тоже меняться, особенно если у вас многоязычное приложение. Наиболее частое решение этой проблемы — это добавление Test ID к необходимым элементам. Если получается договориться с разработчиками, конечно.


  1. tundrawolf_kiba
    18.10.2021 15:29
    +1

    Самый большой минус XPath — что гугл не планирует реализовывать стандарт Xpath 2.0(или даже 3.0). В 3.0, к примеру можно обращаться в JS функциям, что сильно упростило бы работу со строками.


  1. MKMatriX
    18.10.2021 21:41
    +1

    Большая часть примеров по поиск по тексту. На деле конечно эту проблему можно легко решать через js, но видимо для тестов подкручивать js им не охота.


    К сожалению не увидел ни простых описаний XPath, примеры не так легко читаются как кажется автору. Ни крутых примеров, где они элегантно и кратко делают что-то, что через js сделать сложнее.


    В целом XPath нужный и прикольный инструмент, но не для html. Как-то пилил себе на планшет приложение-интерфейс для одного пиратского сайта, и в виду java и не желания тратить ресурсы крайне старого планшета на web-view (цель как раз была избавиться от оного) пришлось пользоваться XPath. К сожалению то что пишется на нем быстро и без изучения выглядит отвратительно, но в целом применение оного к html и предполагало что все будет плохо.


    Ну и да, цеплять что-то за текст на сайте такая себе идея. Видимо вам прокатывает. У себя я бы бил по рукам. И заставил бы добавлять в режиме дебага селекторы, в стиле в сессии debug_selectors = on, и на все что только потребуются добавляются классы в стиле debug_some_name. А то добавится у вас на проект мультиязычность и придется копировать все тесты, плюс если это будет какой-нибудь китайский, то прочитать этот селектор будет уже невозможно.


    ПС. планшет был настолько старым, что для проверки авторизован ли я, пришлось отказаться от поиска через XPath, взять первые 10000 символов и пройтись регуляркой. Получилось в разы быстрее.


  1. saintalushta
    19.10.2021 01:54
    +2

    В общем хотел бы вставить не колько коммкнтов к статье из личного опыта:

    1. Xpath медленее чем css и это видно при большом обращении к поиску элементов в доме. Конечно возможно 3 селениум где-то кэширует большое количество запросов к дому по одному и тому же селектору, но это вряд ли. Медленее потому что xpath рекурсивно обходит дом в отличии от css selector и тут можно холиварить сколько угодно об оптимизации алгоритмов обхода и построения деревьев, но все равно прямая ссылка вниз быстрее чем рекурсивный обход. Кстати поэтому продвинутые инструменты типа сайпреса не поддерживают xpath из коробки)))))

    2. Эти примеры хороши, но на практике фронты используют что-то типа бэм. И тут можео селектор вещать на блок, элемент или модификатор. И если у вас новый элемент в блоке, то упадет и xpath. А повесив на модификатор селектор, можно например фиксировать эти модификаторы на элементах.

    3. К тексту можно привязываться и к всяким атрибутам только в случае, если вы используете xpath как шаблон, который потом параметризируется, а гнать туда статические данные смысла нет от слова совсем. Конечно если у вас не висит стандартная и голая таблица без модификаторов, что в реале вряд ли. Если же вы используете xpath в инициализации полей того же пейджа, то лучше css.

    4. Если у вас фронт не использует бэм или другие best practice, гоните такой фронт в шею. )))) У вас жопа с сапортом кода будет и переиспользованием компонентов и блоков.

    5. Использовать функции хорошо, но ими не нужно злоупотреблять, опять же скорость при большом количестве обращений, а если еще и джава, то плюс время на запуск jvm))))

    6. Ну и как написали выше ребята, развитие xpath под сомнением.


  1. nin-jin
    19.10.2021 08:09
    +2

    И если свойства таких элементов могут измениться, то текст чаще всего останется прежним.

    Текст как минимум меняется взависимости от языка и при использовании интерполяции строк. Не говоря уж о том, что он может просто менятся без изменения логики работы. Лучше задавать каждому элементу уникальный идентификатор, который не будет меняться при редизайне и правке текстов. Подробнее я рассказывал про этот подход в статье Фрактал имён элементов.

    //select[@name=’field[new_object_assignment_key]’]/..//a

    Тут вы очень жёстко завязались на вёрстку. Добавит разработчик доп обёртку и всё, приехали. Корректный селектор с поиском по оси предков или со вложенными xpath запросами будет уже куда менее лаконичным.

    Разработчики зачастую добавляют свои атрибуты ко множеству тегов. Через CSS и стандартными методами фреймворков тестирования их не найти.

    Ну приехали. CSS так-то имеет даже целую коллекцию удобных модификаторов для поиска текста в атрибутах, которых очень не хватает в XPath. Вообще, очень печально, что развитие XPath остановилось вместо того, чтобы объединить лучшие качества CSS и Xpath.


    1. tundrawolf_kiba
      19.10.2021 17:27

      Вообще, очень печально, что развитие XPath остановилось вместо того, чтобы объединить лучшие качества CSS и Xpath.

      Ну строго говоря — развитие самого XPath не остановилось, просто в браузерах используют только версию 1.0 от 99-го года, а версии 2.0(2010) и 3.0(2014), ну и разрабатываемый 4.0 — видимо не будут имплементированы в браузерах никогда.


  1. Doman
    22.10.2021 11:03

    Наверняка в вашем случае, описанный подход оптимален. Но если есть возможность, то, как мне кажется, лучше придерживаться подхода "The more your tests resemble the way your software is used, the more confidence they can give you". Автор этого эмпирического правила набросал небольшую статью на тему, которая задает хороший майндсет.


  1. ysparrow
    22.10.2021 11:17

    Использовать xPath нужно только там, где без него не обойтись, например поиск по тексту )) Во всех оставшихся случаях CSS лаконичнее и гораздо понятнее. От исключительного использования xPath уже в глазах рябит.

    Зачем писать так: //div[contains(@class,'bold') and contains(@class,'nowrap') and contains(@class,'menu-a')]

    Если можно: div.bold.nowrap.menu-a

    Не ограничивайте себя )


    1. nin-jin
      22.10.2021 12:12

      Зачем писать так: div.bold.nowrap.menu-a

      Если можно: //[@my_menu_item]

      Не пишите ерунды)


      1. ysparrow
        29.10.2021 18:12

        Интересно где в каком месте div.bold.nowrap.menu-a находиться my_menu_item ?

        Не пишите ерунды)


        1. nin-jin
          29.10.2021 19:04

          Используйте инструменты, которые позволяют не писать ерунды, тогда у вас не будет никаких div.bold.nowrap.menu-a в принципе.


          1. abaradulkin
            13.11.2021 01:08

            Но откуда вы взяли  //[@my_menu_item]?


            1. nin-jin
              13.11.2021 06:03

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

              В исходном комментарии выше есть ссылка на статью - там всё расписано.