Selenium: Обзор типов локаторов и их подбор.

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

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

  • link= (только для ссылок, естественно, причем при условии, что данная ссылка одна, а не серия)
  • id=
  • name=
  • dom=
  • css=
  • xpath=

Соответственно, когда мы подбираем локатор для некоторого элемента, мы смотрим на его HTML-код и ищем реквизиты:

  • Текст элемента (для статических ссылок это чуть ли не ключевой элемент, для других элементов это как минимум основа для XPath, но вначале лучше смотреть на что-то другое)
  • Атрибут id
  • Атрибут name
  • Соседние или вышестоящие по иерархии элементы, у которых более-менее четко находится хотя бы один из вышепереисленных атрибутов

То есть это как бы основной шаблон, по которому можно подбирать локаторы, соблюдая наиболее оптимальное соотношение точность/скорость выполнения. Но есть несколько типовых случаев, для которых рассматривается некоторое подмножество локаторов, вплоть до программного вычисления нужного элемента.

К этим частным случаям можно отнести следующие:

  1. Отдельная ссылка либо с фиксированным текстом, либо с некоторой фиксированной частью
  2. Стандартный элемент управления формы
  3. Некоторый элемент в таблице, содержащей множество таких же однородных элементов

Теперь можно рассмотреть эти подмножества элементов поотдельности.

Отдельная ссылка либо с фиксированным текстом, либо с некоторой фиксированной частью

Отдельная ссылка с фиксированным текстом

Итак, у нас есть ссылка на странице и мы четко знаем, что она такая одна. Для примера, допустим, у нас есть ссылка

<a href="www.somedomain.com">Sample Link</a>

Теперь подберем локатор, наиболее подходящий для нее, исходя из приоритетов выбора локаторов выше. Что у нас там? Локатор вида link=.

  1. Применяется для ссылок? Да
  2. Использует фиксированный текст ссылки? Да
  3. Идентифицирует ли он этот объект уникально? Да (по условию, такая ссылка одна на странице).

Итак, нам подходит этот тип локаторов. То есть при вызове методов Selenium-a, мы можем использовать локатор link=Sample Link.

Отдельная ссылка с фиксированной частью текста ссылки

Усложним нашу задачу.

Допустим, у нас в тексте ссылки содержится текущая дата, например

<a href="www.somedomain.com">Sample Link Dec 21, 2009</a>

Очевидно, что при задании локатора вида link=Sample Link Dec 21, 2009 все тесты, которые будут использовать данную ссылку перестанут работать корректно, начиная с 22 декабря 2009 года. Но исходя из условия, текст "Sample Link" остается неизменным, то есть у нас есть фиксированная часть. Локатор типа link= использует не просто строку-текст ссылки, а регулярное выражение, совпадающее с текстом ссылки. Соответственно, мы можем применить wildcards, в частности "*". И тогда локатор вида: link=Sample Link*. Уже не будет чувствителен к изменению даты. Опять проверим 3 критерия для ссылок:

  1. Применяется для ссылок? Да (тут ничего не изменилось изначально)
  2. Использует фиксированный текст ссылки? Да (фиксированная часть все-таки присутствует)
  3. Идентифицирует ли он этот объект уникально? Да (по условию, такая ссылка одна на странице).

То есть всё остается по-прежнему.

Несколько ссылок с одинаковым текстом или с одинаковой фиксированной частью

Теперь, допустим, у нас есть 2 ссылки вида:

<a href="www.somedomain.com">Sample Link Dec 21, 2009</a>

Причем, текст у них одинаков и все атрибуты одинаковы (допустим, по дизайну они одинаковы и работать должны одинаково). Если мы по-прежнему будем пользоваться локатором link=Sample Link*, то по умолчанию работа будет вестись с первой ссылкой, которая удовлетворяет данному шаблону. А что делать, если надо проверить работу 2-й ссылки? Проверим 3 критерия для данной ссылки:

  1. Применяется для ссылок? Да (тут ничего не изменилось изначально)
  2. Использует фиксированный текст ссылки? Да (фиксированная часть все-таки присутствует)
  3. Идентифицирует ли он этот объект уникально? Нет (сам объект уже не уникален).

Соответственно, для этой ссылки надо придумать что-то другое. Идем дальше по списку приоритетов:

  1. Ни id ни name атрибутов как правило у ссылок нет.
  2. DOM-локаторы - тоже не самое лучшее решение, когда есть привязка только к тексту или отдельному атрибуту (у нас есть только href).
  3. css - мало чем поможет для неуникальных атрибутов
  4. xpath - как наиболее универсальный тип локаторов, он помогает везде, так как всегда можно детализировать место искомого объекта в DOM-структуре.

Итак, остается расчитывать на xpath. В xpath можно искать как по тексту элемента, так и по значению атрибута, а также по индексу. Итак, нам нужна ссылка ( элемент с тегом "а" ) с текстом "Sample Link" и с индексом 2, причем без привязки к некоторому родительскому элементу. Все эти условия озвучиваются в xpath в виде локатора:

xpath=(//a[contains( text(),'Sample Link')])[2]&nbsp;

Опять применим 3 критерия локаторов для ссылок:

  1. Применяется для ссылок? Да (xpath применим для всех DOM-элементов)
  2. Использует фиксированный текст ссылки? Да (привязка к тексту все-таки присутствует)
  3. Идентифицирует ли он этот объект уникально? Да (есть четкое указание на порядковый номер объекта).

Для данной задачи локатор подходит. Более сложные случаи работы со ссылками будут рассмотрены далее.

Стандартный элемент управления формы

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

<input type=text id=sample_id name=sample_name />

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

  • link= Не подходит, так как объект не является ссылкой
  • id= Подходит, так как атрибут id задан
  • name= Подходит, так как атрибут name задан
  • dom= Подходит, так как все необходимые атрибуты присутствуют
  • css= Подходит, если нет каких-то сложных привязок к объектам на разных уровнях иерархии
  • xpath= Подходит, потому что подходит всегда

Берем самый верхний из подходящих типов и применяем локатор: 

id=sample_id

К несчастью, атрибут id задается далеко не всегда. Иногда даже бывает, что есть несколько элементов с одинаковым значением атрибута id (Земля бoгата талантами, которые могут подобное сотворить, при этом всё решение еще и будет работать). 

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

То есть, для подобных элементов на практике приходится сталкиваться с подобной записью:

<input type=text name=sample_name />

Посмотрим, какие локаторы позволяют описать данный элемент.

  • link= Не подходит, так как объект не является ссылкой
  • id= Не подходит, так как атрибут не id задан
  • name= Подходит, так как атрибут name задан
  • dom= Подходит, так как все необходимые атрибуты присутствуют
  • css= Подходит, если нет каких-то сложных привязок к объектам на разных уровнях иерархии
  • xpath= Подходит, потому что подходит всегда

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

name=sample_name 

или просто

sample_name 

Но тут появляется одна загвоздка. Атрибут name может быть уникальным в пределах своей формы. Но на странице может быть несколько форм и может случиться, что найдутся 2 элемента управления с одинаковым именем. Тогда уникальность уже определяется не только именем элемента, но и формой, в которой он находится. Например, если у нас есть 2 формы вида:

<form name=first_form>
<input type=text name=sample_name />

</form>

<form name=second_form>
<input type=text name=sample_name />
</form>

Допустим, нам нужно ввести текст во второе поле. Опять смотрим, какой локатор лучше подходит:

  • link= Не подходит, так как объект не является ссылкой
  • id= Не подходит, так как атрибут не id задан
  • name= Не подходит, так как атрибут name не уникален
  • dom= Подходит, поле ввода стандартным способом описывается через DHTML в виде document.second_form.sample_name
  • css= Подходит, если нет каких-то сложных привязок к объектам на разных уровнях иерархии
  • xpath= Подходит, потому что подходит всегда

Таким образом, самым верхним локатором оказывается локатор вида:

dom=document.second_form.sample_name 

Есть еще ряд особых случаев, когда надо использовать нечто более сложное. Например, ряд UI-библиотек используют динамические id или же какая-то часть имени генерируется автоматически и в разных версиях приложения эти элементы могут меняться. Например, для нашего предыдущего примера это может иметь вид:

<form name=first_form>
<input type=text name=j0122:sample_name />

</form>

<form name=second_form>
<input type=text name="j0123:sample_name" />
</form>

Сразу бы хотелось отметить, что несмотря на то, что для нужного нам поля атрибут name стал уникальным, локатор name= по-прежнему малоприменим. Мы можем взять шаблон, который маскирует варьируемую часть, но под этот шаблон попадет и первое текстовое поле. Также, локатор dom=document.second_form.j0123:sample_name будет затруднительно использовать, так как символ ":" в DHTML воспринимается как оператор. В этом случае применимы локаторы по css и xpath, так как:

  1. Оба типа локаторов могут работать с иерархией объектов (а мы помимо прочего можем привязаться здесь и к форме, в которой находится нужный элемент)
  2. Мы знаем фиксированную часть атрибута name и можем к ней привязаться, проверить, что такая подстрока входит в значение атрибута.

То есть, мы ищем элемент внутри формы с именем second_form, имя элемента содержит в себе текст 'sample_name'. Как строятся css и xpath локаторы для такого элемента. У нас есть 2 части его описания:

  1. Описание объекта, который содержит в себе искомый элемент: элемент внутри формы с именем second_form
  2. Описание самого элемента: имя элемента содержит в себе текст 'sample_name'

Для начала опишем оба этих элемента поотдельности. Итак, форма с указанным атрибутом name в css записывается вот так:

css=form[name=second_form]

В xpath отсчет обычно идет от самого верхнего элемента в иерархии, но мы точную иерархию не знаем, да и не нужно это нам. В XPath есть такой оператор как "//", который находясь вначале записи XPath буквально обозначает, что последующий за ним элемент находится на произвольном уровне иерархии елементов. То, что нам нужно. Итак, в XPath форма описывается так: 

xpath=//form[@name='second_form'] 

Теперь опишем второй элемент. Мы знаем, что он не вточности равен sample_name, но содержит подстроку. Для css в этом случае мы используем оператор "~=" вместо оператора "=". В XPath для этих целей имеется функция contains. В итоге локаторы для второй части имеют вид:

css=input[name~=sample_name]

xpath=//input[contains(@name,'sample_name')] 

А теперь совместим обе части. Мы знаем соотношение формы и элемента: элемент находится непосредственно внутри формы. В css непосредственный дочерний элемент отделяется от родительского элемента оператором > . В XPath таким оператором является "/". Учитывая это преобразуем локаторы к виду:

css=form[name=second_form] > input[name~=sample_name]

xpath=//form[@name='second_form']/input[contains(@name,'sample_name')] 

Более детально данные локаторы можно рассмотреть на более сложных примерах, к которым мы и переходим.

Некоторый элемент в таблице, содержащей множество таких же однородных элементов

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

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

В качестве примера рассмотрим некоторые типовые случаи:

  1. Выполнение общей операции для всех элементов определенного типа
  2. Выполнение некоторой операции для одного элемента из группы однородных по некоторому критерию
  3. Построчные операции с элементами динамических таблиц

Выполнение общей операции для всех элементов определенного типа

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

<tr>
<td><input type=checkbox name="chbox1"/></td><td>Option 1</td>

<td><input type=checkbox name="chbox2"/></td><td>Some Option</td>
<td><input type=checkbox name="chbox3"/></td><td>Another Option </td>
<td><input type=checkbox name="chbox4"/></td><td>Option 4</td>
</tr>

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

В частности, такой общий локатор полезен для вышеуказанной операции. Нам надо пройтись по всем элементам и для каждого из них вызвать операцию установки флажка. Итак, что объединяет все эти элементы? То, что все эти элементы это объекты input у которых атрибут type равен checkbox. То есть общий локатор должен учитывать эти 2 момента. Из всех типов локаторов с типом элемента и его атрибутами оперируют только 2 вида:

  1. css=
  2. xpath=

Общие локаторы имеют вид, соответственно:

css=input[type=checkbox]

xpath=(//input[@type=checkbox]) 

Но это общий локатор и вот в таком виде как есть он соответствует первому элементу из всей группы элементов. Для того, чтобы обратиться ко второму, третьему и т.д. элементам, надо указать порядковый номер, он же индекс. А индекс из локаторов оставшихся типов поддерживается только в Xpath. Например,

xpath=(//input[@type=checkbox])[2] 

обращается ко второму чек-боксу на странице. А для извлечения общего количества данных элементов используется метод getXPathCount. В итоге, код выполняющий одно и то же действие для всех однородных элементов имеет вид (рассмотрим на примере вызова метода check):

int count = getXPathCount( "xpath=(//input[@type=checkbox])" ); 
for( int i = 1 ; i <= count ; i++ ){
    check( "xpath=(//input[@type=checkbox])[" + i + "]" );
}

Выполнение некоторой операции для одного элемента из группы однородных по некоторому критерию

Вернемся к таблице из предыдущего примера:

<tr>
<td><input type=checkbox name="chbox1"/></td><td>Option 1</td>

<td><input type=checkbox name="chbox2"/></td><td>Some Option</td>
<td><input type=checkbox name="chbox3"/></td><td>Another Option </td>
<td><input type=checkbox name="chbox4"/></td><td>Option 4</td>
</tr>

Допустим, мы знаем опцию, напротив которой надо поставить флаг. Допустим, это будет опция Some Option и нам надо, зная имя опции, установить флаг в нужном чек-боксе. Итак, нам надо обратиться к элементу input типа checkbox, который находится в ячейке таблицы, в строке, в которой в другой ячейке находится текст "Some Option".

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

Теперь более детально разберем выделенный выше текст, постепенно достраивая нужный нам XPath. Нам надо обратиться к элементу input типа checkbox. Тут тривиально:

//input[@type='checkbox']

который находится в ячейке таблицы. Тут надо обратиться к ячейке, причем из HTML-кода видно, что в строке эта ячейка первая. То есть, родительский элемент у чек-бокса - ячейка с индексом 1:

//td[1]/input[@type='checkbox']

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

//tr/td[1]/input[@type='checkbox']

в которой в другой ячейке находится текст "Some option". Вот тут загвоздка. Единственное, что мы знаем про нужный нам элемент tr - это то, что в нем есть ячейка с текстом "Some Option". То есть, для самой этой ячейки XPath может иметь вид:

//tr/td[text()='Some Option']

Теперь, поставим рядом оба полученных XPath:

//tr/td[text()='Some Option']

//tr/td[1]/input[@type='checkbox']

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

//tr/td[text()='Some Option']/..

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

//tr/td[text()='Some Option']/../td[1]/input[@type='checkbox']

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

Построчные операции с элементами динамических таблиц

Практика показывает, что XPath из предыдущего примера вызывает эмоции вида: 

  1. OMG!!! Что это?!!
  2. Если я это попытаюсь прочесть, я сломаю глаза
  3. Если там в UI что-то перекосится (произойдет сдвиг или добавятся элементы), проще будет переделать заново этот локатор
  4. Придется молиться, чтобы этот локатор просто медленно работал под IE (в этом браузере XPath-локаторы работают особенно медленно), а ведь может и не сработать

В любом случае, можно сделать то же самое, но более удобно для понимания. К тому же, при работе с динамическими таблицами, зачастую полезно иметь дополнительный функционал, который:

  1. Извлечет все значения какой-то колонки (зачастую это имена объектов, которые отображены в таблице)
  2. Сделать некоторое действие над однородным элементом в конкретной строке

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

Чтобы выполнить эту операцию для всех чекбоксов, нам нужно знать, сколько их всего. Для этого в Селениуме предусмотрена команда getXPathCount, которая вернет количество элементов, удовлетворяющих выражению, передаваемому параметром. Соответственно, мы делаем вызов вида: 

int count = selenium.getXPathCount( "//input[@type='checkbox']" ).intValue();

После этого мы можем в цикле перебрать все чекбоксы (просто можно варьировать индекс) и обращаться к нужной ячейке. В целом код выглядит так:

    String data[] = new String[count];
for( int i = 0 ; i < count ; i++ ){
    data[i] = selenium.getText( "xpath=(//input[@type='checkbox'])[" + ( i + 1 ) + "]/../../td[2]" );
}

Другой пример: нужно установить флаг для всех чекбоксов, имя которых содержит слово "Option". Опять же, надо определить, сколько всего таких элементов, затем пройтись по именам элементов, после чего установить флаг, если имя элемента содержит слово "Option". Делается это так:

int count = selenium.getXPathCount( "//input[@type='checkbox']" ).intValue();
for( int i = 0 ; i < count ; i++ ){
        String text = selenium.getText( "xpath=(//input[@type='checkbox'])[" + ( i + 1 ) + "]/../../td[2]" );
    if( text.contains( "Option" ) ){
        selenium.check( "xpath=(//input[@type='checkbox'])[" + ( i + 1 ) + "]" );
    }
}

Вот ряд типичных случаев подборов локаторов. Безусловно, в ряде случаев придется комбинировать подходы, искать какие-то специальные решения или же делать какие-то допущения. Опять же, для разных браузеров те или иные локаторы могут работать очень долго, например, те же XPath-локаторы под IE работают крайне медленно. 

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

15 лайков

Поправьте опечатку

1 лайк

Done

Хм, а я думал так было задумано… “…БАГата…” ))))

Есть ли статьи с реальными тестами производительности локаторов? Интересны только современные браузеры chrome, firefox, edge. Также хотелось бы отметить что xpath это универсальный локатор который может искать в любой иерархической структуре данных с тэгами, атрибутами и значениями тэгов(xml). А css изначально придуман для описания вэб стилей. То есть использовать css предпочтительнее там, где это возможно, если не учитывать скорость работы ибо данных нет.

Я где-то полгода назад в ядре АТ переписал целиком всё, что касается поиска элементов.
После этого все написанные локаторы приводятся в конечном итоге к XPath перед поиском элемента.
Время прохождения тестов осталось таким же для ff и chrome, остальное не использую.
Так что для себя сделал вывод, что нет смысла использовать что-либо кроме XPath, особенно если хорошо с ним дружишь.
Помню из прошлого только тот факт, что XPath раньше в IE ужасно медленно работал. Как сейчас с этим дела обстоят - хз.

3 лайка

Оказывается статья то 10 года! Тогда история про IE понятна. Возможно, сейчас локаторы работают примерно одинаково по скорости. Но вот поиск по id я уверен работает быстрее. Это типа как индексация в бд. И так как до сих пор видимо нет серьезного сравнения производительности локаторов по всем правил подобных экспериментов на достаточной выборке, то тема ещё актуальна.

Если я не ошибаюсь, то поиск по ID в конечном итоге приводится к CSS селектору. Как минимум в шарповом селениуме.