воскресенье, 23 сентября 2018 г.

XPath в картинках

XPath - язык запросов, позволяющий обращаться к элементам XML-документа.
XPath активно используется в мире автотестов веб (ибо веб-страница - частный случай XML-документа).

В целом XPath штука несложная, если разобраться с двумя основными концепциями: осями и предикатами.



XPath axes - оси XPath

Оси - это направления поиска относительно текущего элемента. Они помогают разбить документ на несколько множеств и указать, в каком из этих множеств искать нужный элемент.
Не все из существующих осей широко применяются в контексте веб-автотестов (например namespace:: не имеет особого смысла). Некоторые оси применяются постоянно, но не всегда это очевидно (self:: или descendant-or-self::).

Список существующих осей:
Чтобы разобраться со смыслом основных осей, рассмотрим выдуманную веб-страницу. Как всякий XML-документ, веб-страницу можно рассматривать как дерево, узлы которого пронумерованы сверху вниз и слева направо.

Чтобы было легче видеть за этим деревом веб-документ, опишем эту же структуру стандартными HTML тегами:

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

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

Ось self:: позволяет обратиться к текущему элементу, если это зачем-то нужно.
Краткая нотация этой оси - точка.
У элемента есть предки и потомки.
Предки - элементы, расположенные на пути от корня дерева до текущего элемента. Множество предков лежит на оси ancestor::.
Потомки - элементы, расположенные в дереве ниже текущего элемента. Они лежат на оси descendant::.
В терминах HTML тегов предки - это теги, которые не были закрыты до того места, где был открыт тэг текущего элемента. А потомки - это элементы, расположенные между открывающим и закрывающим тегами текущего элемента.

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

Кстати, все приведённые пути начинаются точки, то есть с оси self::, чтобы задать текущий элемент в качестве точки отсчёта. Без этой точки поиск начнётся от корня дерева.
Также стоит отметить правило нумерации: элементы оси всегда нумеруются относительно текущего элемента: ближайший элемент имеет номер 1, следующий 2 и так далее. Можно искать среди всех элементов лежащих на оси или указать имя тега, чтобы сузить пространство поиска. Соответствующим образом меняется нумерация: так, td11 - четвёртый среди всех потомков, но третий элемент td среди потомков. Поэтому запросы ./descendant::*[4] и ./descendant::td[3] в данном случае эквивалентны.

Следующие две оси - parent:: и child::, означающие, соответственно, родителя (он всегда ровно один, если речь не о корневом элементе) и детей текущего узла. 
У этих осей есть краткие нотации.

Примеры поиска элементов на этих осях:

Идём далее.
Предшествующие (preceding::) и последующие (following::) элементы. 
В терминах дерева предшествующие - это элементы, чьи порядковые номера меньше номера текущего элемента, за исключением предков. Последующие - элементы с номерами большими, чем у текущего за исключением потомков.
В терминах HTML предшествующие - это элементы, теги которых были закрыты до открывающего тега текущего элемента. А последующие - те, чьи теги были открыты уже после закрытия тега текущего элемента.

На практике использование этих осей я встречал редко, но на всякий случай вновь примеры доступа к элементам лежащим на этих осях:
Ещё две интересные оси - preceding-sibling:: (предшествующие братья или старшие братья) и following-sibling:: (последующие или младшие братья).
Как следует из названий, на этих осях расположены элементы, имеющие того же родителя, что и текущий элемент, и имеющие меньшие или большие порядковые номера.
Примеры XPath для этих осей:
Кроме перечисленных, есть ещё несколько осей, которые на этих картинках не видны.
Ось атрибутов attribute:: (или @ в краткой нотации) позволяет обратиться к атрибутам элементов: например, атрибуту href элемента а9.
text() позволяет найти текстовые элементы документа (которые, в частности, есть между любыми двумя соседними тегами, даже если там нет явно написанного текста).
node()  - узловые элементы документа.
* - узловые и текстовые элементы.
Также есть оси ancestor-or-self:: и descendant-or-self::, смысл которых ясен из названия, и которые редко используются в явном виде. 
Зато часто используется запрос /descendant-or-self::node()/ более известный виде краткой нотации //.
На этом про оси всё. Напоследок ещё раз список осей, с краткими нотациями:

Предикаты XPath

Предикаты XPath - это дополнительные условия, помогающий уточнить запрос.
На самом деле в примерах выше постоянно использовался простейший предикат - указание позиции элемента.
Однако пользоваться только позицией было бы не очень удобно, особенно в мире динамических веб-страниц.
Предикаты позволяют указать значения атрибутов искомого элемента.
Пример ниже показывает, какое множество элементов находит тот или иной запрос в HTML коде справа.
Также популярны предикаты работающие  текстом внутри элемента: "text()" и ".".
На первый взгляд они выглядят похоже: оба запроса для span на картинке ниже возвращают одно и то же.
Однако если посмотреть на запросы для кнопок - возникают вопросы: 
  • //button[text()="Click"] не находит ничего, 
  • //button[.="Click"]] находит нужную кнопку, а
  • //button[.="Press"]] вновь ничего не находит.
Причина в том, что предикат text()="something" ищет элемент, у которого есть собственный текстовый узел с таким значением. А .="something" ищет элемент, текстовое представление которого совпадает с указанным значением. При этом текстовое представление элемента учитывает все текстовые узлы внутри элемента - как собственные, так и дочерних элементов. Иными словами, тестовое представление узла - это конкатенция всех текстовых узлов внутри элемента.
Поэтому
  • //button[text()="Click"] не находит ничего - ведь "Click" содержится не в собственном текстовом узле кнопки,
  • //button[.="Click"]] находит нужную кнопку: текстовое представление кнопки именно "Click"
  • //button[.="Press"]] вновь ничего не находит, так помимо "Press" текстовое представление кнопки содержит ещё и два переноса строки.
Кроме того имеется набор функций, позволяющих сравнивать строки, считать элементы и т.д. Подробно останавливаться на этом не буду.


Хороший XPath

Напоследок некоторые мысли, что отличает "хороший" XPath-запрос от "плохого" в контексте веб-тестирования.
Хороший XPath должен быть
  • точным - указывать на нужный элемент страницы
  • уникальным - указывать в идеале ровно на один элемент (а не просто на первый из массы похожих)
  • быстрым - быстро находить элемент на странице
  • не хрупким - таким, чтоб не приходилось его менять после каждого изменения вида страницы
  • описательным и лаконичным - то есть по возможности коротким, но при этом хорошо, если читая XPath-запрос человек понимает, что этот запрос ищет (да, это немного противоречивые требования).
Все эти критерии ситуативны и требуют применения здравого смысла.
Обычно не возникает проблем с точностью и скоростью поиска. 
С другими тремя пунктами сложнее.

Чтоб было понятнее, о чём речь, приведу примеры.

Не уникальный XPath: //span[contains(text(), 'already exists')] - сложно сказать сколько подобных элементов окажется на странице. Хотя, в контексте конкретного теста мы можем точно знать, что такой элемент ровно один.

Хрупкий XPath:
  • //span[text()='Open']/../../button[contains(@id, 'actions-button-%s')]
  • //span[contains(text(),'%s')]/..//div[@class='panel']/div/span/div/span[3]//span[1]

Лесенки вверх (../../),  длинные списки элементов (/div/span/div/span[3]//span[1]) это те места, которые могут легко и неожиданно ломаться при изменении веб-страницы.

Лесенки в большинстве случаев можно устранить, используя вместо них предикаты и вложенные запросы
//button[span[text()='Open']][contains(@id, 'actions-button-%s')]


Не лаконично-описательный XPath:
  • //span[@aria-label='%s. Press the Enter key.']/../..
  • //p[text()='%s']/../../../td[1]/input
  • //td[text()='%s']/following-sibling::td[count(//div[@id='usage_stat']//th[text()='Amount']/preceding-sibling::th)-1]
Чтобы понять, что ищут эти запросы, нужно в лучшем случае очень внимательно читать запрос (а в худшем - нет ни одного шанса понять, не проверив непосредственно на странице).
Кстати, эти не лаконично-описательные XPath'ы одновременно ещё и хрупки. Есть ощущение, что часто эти свойства ходят парой: если сломано одно из них, до и другое скорее всего тоже.

Иногда полезно вообще попробовать использовать вместо XPath другой инструмент. Например, вместо построения сложного запроса ищущего что-то в таблице (&lttable&gt), может оказаться проще вычитать всю таблицу в хэш-таблицу (aka мапу, словарь) и потом вычитывать нужные данные оттуда.

На этом всё.
Хороших XPath'ов.



Комментариев нет:

Отправить комментарий