На сайте hh.ru есть около 100 вакансий, где навык составления XPath важен для работодателя, также в интернетах полно материалов, вроде шпаргалок по составлению локаторов или ворк-шопов на ютубе. Как-то у меня спросили на собеседовании про то, какой из языков построения локаторов использовать лучше XPath vs CSS, и я ответил — лучше использовать тестовые аттрибуты, а если мы их используем то и использовать эти языки необязательно. Скорее всего такой ответ не устроил, но я ответил честно, т.к на предыдущем месте мы старались не использовать XPath для решения этой задачи.
Что это за зверь XPath
XPath (XML Path Language) — это язык путей, использующий синтаксис, отличный от XML, для обеспечения адресации различных частей XML-документа. Существует несколько стандартов данного языка, XPath 3.1 - опубликован в 2017 году (с поддержкой карт, массивов и JSON).
Селекторы XPath обычно называются «xpaths», и один xpath указывает пункт назначения от корня до желаемой конечной точки.
Операторы
Операторы и
/
, //[...]
Оператор объединения узлов,
|
Булевы операторы
and
иor
, и функцияnot()
Арифметические операторы
+
,-
,*
,div
иmod
Операторы сравнения
=
,!=
,<
,>
,<=
, >=
Функции
concat(), substring(), contains(), substring-before() и многое другое
Как это используется в тестах
В тестах мы вынуждены взаимодействовать с элементами HTML-страницы, а для этого нам нужны селекторы, чтобы обращаться к кнопкам, формам ввода, и другим элементам веб-интерфейса.
К примеру, у нас есть простой html-документ, и нам нужно обратиться к элементу а, находящемуся внутри div
<head>
<title>Xpath & CSS</title>
</head>
<body>
<h1>Hello World</h1>
<p>some description text: </p>
<div>
<a class="link" href="https://helloworld.com">example link</a>
</div>
</body>
XPath-selector
/html/body/div[1]/p/a
CSS-selector
body > div > a
В тестах можем обращаться к этому локатору так
WebElement passwordByXPath = driver.findElement(By.xpath("/html/body/div[1]/p/a"));
//или вот так
WebElement passwordByXPath2 = driver.findElement(By.xpath("//a[contains(text(),"example link")]"));
WebElement passwordByCSS = driver.findElement(By.css("body > div > a"));
Почему этим пользуемся и почему это не очень
Предположу что есть несколько причин, по которым этот подход еще используется:
исторически так сложилось
фронтенд не подготовлен для разработки автотестов (разработчики не предусмотрели тестовые локаторы-аттрибуты), выкручиваемся запросами c условиями
Если причиной является отсутствие уникальных тестовых аттрибутов и мы строим локатор, исходя из условий, скорее всего мы создаём нестабильный локатор, в не зависимости от того, насколько правильно он составлен.
Если мы завязались на название класса, то в любой момент он может измениться, например при пересборке веб-пака, а при изменении расположения или при оборачивании в новый tag - изменится путь и локатор будет невалидным.
Для тестов нам нужен стабильный, уникальный локатор, для этого правильным решением будет использовать тестовый аттрибут.
Преимущества использования тестовых аттрибутов
повышается стабильность, т.к не используются условия, функции
снижение вариативности
поддержка автоматической нумерации (для множественных объектов одного типа)
Из минусов — упрощается парсинг, если локатор доступен извне.
Поддержка тестовых атрибутов популярными фреймворками
фреймворк |
использование |
тестовые аттрибуты |
Playwrigth |
page.getByTestId('directions').click(); |
data-pw |
Cypress.io |
cy.get('[data-cy="submit"]').click(); |
data-cy |
WebdriverIO |
$('button[data-testid="submit"]') |
data-testid |
Если внутри компании не принято решение о кастомном названии для тестовых аттрибутов, то можно использовать data-testid
, которое поддерживается и рекомендуется большинством фреймворков для использования.
В Selenium из коробки отсутствует поддержка тест-аттрибутов, но можно выкрутиться вот так, а в дальнейшем, можно поддержать тестовое название внутри проекта
driver.findElement(By.css('[data-testid="entry-btn"]')
Как расставить тестовые локаторы
Самое простое решение — создать задачу на доработку фронтенда, и закинуть в беклог, а затем при планировании, взять её в спринт, согласно приоритету. Но, если есть свободное время и желание, можно самостоятельно локально поднять и поковырять фронтенд. Обратить внимание на компонент, если он существует.
Вариант первый (и самый простой) — добавление локатора напрямую
import React, { Component } from 'react';
import { Button } from "@material-ui/core";
function ViewIntro({ onOrder }) {
return (
<Button
variant=”contained”
color=”primary”
data-testid=”entry-btn” //просто добавляем аттрибут здесь и проверяем через консполь
onClick={onOrder}>
Order your ????
</Button>
);
}
А можно добавить data-testid в сам компонент и при использовании компонента передать значение dataTestId
const Component = ({ variant, color, dataTestId, onOrder }) => (
<Button
variant={variant}
color={color}
data-testid={dataTestId}
onClick={onOrder}>
Order your ????
</Button>
);
Итог
строить локаторы, используя все возможности XPath/СSS - на основе названий классов, вложенности, и всего, что может в любой момент измениться — плохо
для автоматизации желательно использовать уникальные тестовые атрибуты
для тестовых атрибутов можно использовать название
data-testid
, которое поддерживается несколькими фреймворкамидля обращения к этим тестовым локаторам можно использовать внутренние методы или реализовать свой собственный, а сами локаторы поместить в привычный PageObject класс
Спасибо за прочтение
Комментарии (27)
smple
15.06.2023 21:59+4просмотрел статью по диаганали, по озвученным проблемам уже давно существует решение в виде page object, отсюда в тестах не используется xpath, а используется page object в котором уже скрыта (инкапсулирована) работа с xpath.
Итог: не обязательно добавлять data-testid можно использовать обычные id, class, tag если они есть, вопрос именования это отдельная тема, не будем спорить нужен ли БЭМ или альтернативы.
Добавляя data-testid мы не перестаем использовать xpath, мы вместо использование классов, тэгов, id. Начинаем использовать пользовательские атрибуты.
iBljad
15.06.2023 21:59+2Куда бы ни был инкапсулирован локатор, тесты все равно упадут, если произойдут озвученные риски (смена верстки итд, но и с "data-testid" оно может упасть при кардинальных изменениях, если уж на то пошло).
smple
15.06.2023 21:59так кто вам гарантирует что при изменение верстки тот кто верстал не посносил data-testid ?
тесты на то и тесты чтобы не заливать то что не проходит тесты, поэтому тот кто поменяет верстку, будет обновлять и page object, тесты ему напомнят что упало и на что обратить внимания, для этого они и пишутся.
например пусть была некая колонка с балансом в профиле пользователя, а после редизайна она перехала куда то в другое место, на другую страницу (мой баланс например и там текущий баланс и операции пополнения и расходов) как вы ее будете находить не меняя тестов ? будете оставлять колонку на баланс в профиле с display: none ? (костылище)
как в этом случае мне поможет подход озвученный вами ?тоесть при подобном кейсе в моем варианте.
Разработчик обновляет верстку (редизайн и тд) сфокусировавшись на этом.
Запускает тесты чтобы заметить регресс.
Исправляет регресс у упавших тестов.
MR
В вашем случае на первом этапе помимо фокусирования на задаче, разработчик еще будет смотреть data-* атрибуты и где они используются в тестах, получится он на первом шаге будет делать еще и второй и третий этапы.
где тут отказ от xpath/css селекторов ?
ваше предложение сводится к тому что используйте пользовательские атрибуты в xpath/css селекторах в тестах вместо class/id/tag.
Вот я не согласен на "вместо" я считаю что надо использовать там где возможно стандартные селекторы основанные на tag/class/id, а где необходимо можно добавлять и пользовательские атрибуты, но не строить тестирование только вокруг пользовательских атрибутов.
Чем предложенный вами data-testid отличается от обычного id ?
Почему человеку надо вместо id использовать data-testid ?причем если посмотреть описание id http://htmlbook.ru/css/selector/id
Идентификатор (называемый также «ID селектор») определяет уникальное имя элемента, которое используется для изменения его стиля и обращения к нему через скрипты.
я даже выделил важное в цитате, как раз то как вы собираетесь использовать его.
smple
15.06.2023 21:59как то я слишком расплывчато написал, попробую кратко по пунктом сумировать
вместо data-testid можно, а возможно даже лучше использовать стандартный id
пользовательские атрибуты (data-*) нужно использовать по назначению там где нет стандартных.
для того чтобы изолировать тесты от структуры документа и не использовать селекторы в тестах можно использовать page object, но это надо далеко не всем
использование пользовательских атрибутов это не значит что вы отказались от css селекторов.
kulykovdmytr Автор
15.06.2023 21:59В вашем случае на первом этапе помимо фокусирования на задаче, разработчик еще будет смотреть data-* атрибуты и где они используются в тестах, получится он на первом шаге будет делать еще и второй и третий этапы
Совсем не уверен, что разработчику нужно смотреть как эти локаторы используются в тестах. Если разработчик видит этот локатор, значит будет держать в уме, что в тестах этот локатор используется, это и помешает ему посносить. Во всяком случае, в моей практике такого не было. Продублировать не получится, подскажет умная IDE…
ID, так же, ка к и всё остальное — может в любой момент измениться, причем разработчик спокойно может в любой момент это изменить, без согласования, при этом тесты не сломаются, поскольку не завязаны на классы, теги и обычные айдишники.
up40k
15.06.2023 21:59Page object - хороший, годный слой абстракции. Сделал такой вывод, почитав по диагонали его концепцию в доках Селениума.
Он не решает озвученную проблему. Да, вы переносите описание локатора непосредственно из теста в какое-то общее место (описание структуры документа), отделяя его от ассертов, но вам всё ещё нужно учитывать нюансы выбранного способа локации.
Предлагаете доступ к элементам по атрибуту id? Прекрасно. Только разрабу всё равно придётся эти айдишники проставлять для вашего удобства. А потом в проде вас будут нещадно парсить, блочить рекламу и прочее.
Почему бы не договориться, что в тестовом (предпродовом, если угодно) окружении, где проходят наши тесты, будут использоваться нужные юзер-атрибуты? Да, это нагрузка на разраба, ему нужно будет сверяться с таблицей (которую должен составлять тестировщик, тут отдельная тема того, что эту таблицу нельзя прописать в код page object, но можно описать в документации) и вручную прописывать нужные атрибуты после правки кода и перед пушем. Но тут мы не трогаем id, которые используются в коде приложения и могут быть рандомными. Перед пушем в прод достаточно прогнать скрипт, который убирает из кода добавление юзер-атрибутов.
Да, то же самое можно с делать с id. И в зависимости от окружения выдавать разные значения этого атрибута (статик в тест, динамик в прод). Я прикинул, в этом просто больше кодинга, а нагрузка на дева такая же (или больше, кодинга же больше).
> 4
Да, но глобально - это вопрос унификации взаимодействия. Тестировщика с разработчкиом, тестов с кодом, пользователя с продуктом. Как я написал в каменте ниже, нужно ориентироваться на представление пользователя. И page object, и юзер-атрибуты, и локаторы на основе ARIA вместо селекторов - инструменты, которые мы можем применять в конкретном проекте
В любом случае, это всё не панацея для выстраивания процесса тестирования. Это может в той или иной степени помочь, и, в зависимости от проекта, быть применимо или нет.kulykovdmytr Автор
15.06.2023 21:59Кстати говоря, расставлять локаторы не обязательно разработчику, возможно стоит один раз показать, а дальше просто подключаться на ревью простых задачек на добавление этих локаторов.
В дальнейшим эти локаторы попадут в PageObject и будут использоваться в тестах.
Решение с тестовыми аттрибутами используется, причем многими компаниями, мой поинт был отказаться от построения XPath/CSS путей, завязанных на сущности, которые меняются.
iBljad
15.06.2023 21:59так кто вам гарантирует что при изменение верстки тот кто верстал не посносил data-testid
я специально вроде в скобках написал, что не всегда есть гарантия, но некоторую защищенность в этом плане может обеспечить, например, использование библиотеки компонентов, где data-testid прописан именно для компонентов, а не только в верстке.
Чем предложенный вами data-testid отличается от обычного id
лично я ничего не предлагал, но у меня есть ответ: в некоторых популярных фронтенд-фреймворках id может быть автогенерируемым (возможно, чтобы избежать использования кеша для старых стилей, но это не точно)
PS: Глядя на комментарии под статьей, хочу акцентировать, что нигде не писал такие фразы, как "нужно так делать" и "всем 100% подойдет этот подход" :)
XaBoK
15.06.2023 21:59А в чём смысл писать "
/html/body/div[1]/p/a" или "//a[contains(text(),"example link")]"
вместо "//a"?iBljad
15.06.2023 21:59+1потому что на веб-страницах обычно больше одной ссылки и их надо как-то различать?
XaBoK
15.06.2023 21:59И как их различает "
body > div > a"?
Если мы не в вакууме, то тут уже много вариантов: по положению(путь/индекс), по атрибутам, по значению. Хотите как в CSS
//body/div/a или
? Просто тут явно хотели некрасивый и нефункциональный X-Path. Это обесценивает всю статью.up40k
15.06.2023 21:59Это не обесценивает всю статью, просто примеры как для XPath, так и для CSS-Selectors нерелевантные.
kulykovdmytr Автор
15.06.2023 21:59В этом и проблема — много вариантов. Можно написать хорошо, можно не очень, а можно вообще не использовать язык для обращения к локатору, а обращаться однозначно, зная что он уникален, если позволяет фреймворк.
lxsmkv
15.06.2023 21:59+1Спасибо что обратили внимание на data-аттрибуты. Это часть стандарта и очень мощный инструмент.
https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
Я не вижу никаких аргументов против введения таких аттрибутов в код фронтенда.
Это часть "testability". И висеть в проде эти аттибуты тоже не обязательно должны. Можно заставить компилятор или препроцессор убрать эти аттрибуты при сборке.Да, придется вложиться в это. Но это и есть серьезное отношение к автоматизации тестирования. Бывает, конечно, что тестируемость случайно возможна, посредством каких нибудь манипуляций, или из-за свойств системы, но полагаться на это не стоит. Тестируемость это инвестиция, инвестиция в будущее сложной системы. И она стоит денег.
А автоматизация тестирования, это разработка программного обеспечения. И не нужно делать вид что "ну че там пару скриптиков написать чтобы кнопочки потыкать". Чем дольше притворяться, что автоматизация тестирования это не "программирование систем", тем дольше она будет у вас непрофессиональной и нездоровой.
Часто такое мировоззрение в компаниях в которых DevOps - это "чувак который настраивает Jenkins".
up40k
15.06.2023 21:59+1В Selenium из коробки отсутствует поддержка тест-аттрибутов
Судя по вашим примерам в таблице, основанным на common CSS-селекторах, она отсутствует везде, кроме Playwright.
Получается, мы просто используем соглашение между фронтом и QA для удобной автоматизации тестирования. Есть серьезный недостаток, который вы не затронули, он касается излишнего автоматизма автоматизации (простите за тавтологию): на фронте может быть поломан UX, но при этом тесты будут проходить. Нам так или иначе нужно отталкиваться от представления пользователя, от того, как он взаимодействует с приложением (страницей, документом).
В том же Playwright есть локатор page.getByRole(), который призван если не решить, то помочь в решении с представлением пользователя. Само собой, это можно и нужно комбинировать с использованием тестовых атрибутов, если есть такая возможность.
kulykovdmytr Автор
15.06.2023 21:59Плохо представляю, каким образом тесты будут проходить, если поломан UX…
Если тест написан правильно и в нём есть ассерты, он не сможет пройти..
getByRole(), кажется, создан для других задач, но через него действительно можно обращаться к аттрибутам, но вопрос - зачемawait page.getByRole('checkbox', { name: 'Subscribe' }).check();
up40k
15.06.2023 21:59Если тест написан правильно и в нём есть ассерты, он не сможет пройти..
Сможет. В зависимости от вашего "правильно". Вы заполняете форму, получаете нужный ответ. А пользователь не может. Потому что разраб добавил шифрование пароля, например, поменяв id элементов (используя финты с динамической скрытой формой). Но где-то прошляпил с уровнями иерархии. В итоге ваш тест работает, а в проде не работает.
кажется, создан для других задач
Для тех самых, чтобы абстрагироваться от структуры DOM и его атрибутов.
через него действительно можно обращаться к аттрибутам
Ну нет же, вы выбираете элемент/набор элементов по ARIA (то, что доступно пользователю), а уже после проверяете атрибуты с помощью фильтров. Где-то там оно должно упасть в случае косяка разраба.
kulykovdmytr Автор
15.06.2023 21:59Вашу проблему подсветит результат сценарного е2е теста: "успешная авторизация и переход в личный кабинет (например)". Здесь я имею в виду не переход в личный кабинет через ссылку или параметр next в query, а как результат абстрактной, успешной авторизации. В тесте проверяется весь путь, дополнительно можно проверить простановку необходимых кук.
Если честно, не понял, как то, что вы подсветили связано с аттрибутами, ну ладно.
getByRole создан для поиска аттрибутов, в том числе созданных неявно. Это решение может подойти для тестовых локаторов с названиями, которые не поддерживаются getByTestId() или если getByTestId() отсутствует в фреймворке.
VadimZubovich
15.06.2023 21:59+1Подход, который зачастую упирается в нежелание разработчиков писать тестовые атрибуты и нежелание менеджеров эту практику правильно прививать. Однако если взлетает, то жить становится и правда легче.
Добавлю только, что в случае с Селениумом:
driver.findElement(By.css('[data-testid="entry-btn"]')
Можно просто заэкстендить класс By, добавить туда метод testId():
public static By testId(String selector) { if (selector == null) { throw new IllegalArgumentException("Cannot find elements when locator is null"); } else { return new ByCssSelector(String.format("[data-testid='%s']", selector)); } }
И после этого вполне себе использовать радостно свои тест-айдишки, импортируя свой соответствующий By, вместо селениумовского:
driver.findElement(By.testId("entry-btn"))
kulykovdmytr Автор
15.06.2023 21:59Реализация определяется фреймворком и его возможностями, если обертки нет, то можно сделать так, как предложили вы, спасибо, что подсветили. Я не стал на это акцентировать внимание.
antonkrechetov
15.06.2023 21:59Совершенно очевидно, что тесты должны быть привязаны именно к тому, к чему привязан внешний вид/поведение фронтенда. Если у вас обычная верстка по БЭМ и сырой JS типа
document.querySelectorAll('.button__delete-all-users').addEventListener(...)
то любые варианты будут хуже селекторов классов. Если же, как в вашем примере, Реакт и material-ui, и вы, вообще говоря, не можете гарантировать какой DOM у вас будет на выходе, то тогда, конечно, дата-атрибуты выгядят неплохим вариантом. Только это костыль, который приходится делать за неимением лучшего.
iBljad
Знать xpath все равно стоит для случаев, когда:
Локатора ещё нет, а тесты нужны
Локаторы есть, но у вас имеется структура элементов вида data-testid=my-list: data-testid=my-item, (где у каждого элемента в списке может быть ещё и data-testid=my-title, и вот по нему и надо найти)
kulykovdmytr Автор
Лучше, если сначала фронтенд будет готов для тестов, чем писать тесты и затем исправлять старые локаторы на новые…
Если у каждого элемента внутри вложенности есть свой тайтл и айтем, это исправляется со стороны фронтенда, в тестовый локатор передается id элемента, и для каждого элемента локатор продолжает быть уникальным.
iBljad
Конечно лучше быть здоровым и богатым, чем больным и бедным (с), но ждать "готовности" фронта можно долго, т.к. такие доработки обычно не в приоритете, но основная цель тестов -- помогать в обнаружении ошибок/экономить время на регресс, а не быть написанными на века, поэтому оптимальнее их все-таки создать раньше и на том, что есть, а потом обновить локаторы, все-таки их правка занимает меньше 5% от времени на разработку тестов.
Или я не так объяснил, или не так был понят, давайте на примере (искусственном, поэтому без вопросов к логике)): у вас есть страница покупок, где есть два списка: "купить" и "куплено", вам нужно проверить, что итем переехал из одного списка в другой, поэтому просто сверки по тайтлу будет мало, нужно еще учитывать родителя, тут одним лишь test-id не обойтись.
kulykovdmytr Автор
Проверить появление нового аттрибута у айтема можно силами фреймворка через hasAttribute()/toHaveAttribute(), а ещё, купленный айтем может имет совершенно другой локатор data-testid-bought(itemId)..
iBljad
Это не новый атрибут, сам элемент не изменяется, меняется дерево -- элемент переезжает, меняя родителя (со списка "купить" на "куплено").
Про другой локатор -- я специально написал, что пример искусственный (но имеющий аналоги на практике, просто решил не усложнять лишними деталями)