На сайте 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
Проверить локатор можно через консоль браузера $$(''); для CSS, $x(''); для XPath
Проверить локатор можно через консоль браузера $$(''); для CSS, $x(''); для XPath

В тестах можем обращаться к этому локатору так

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
data-testid

Cypress.io

cy.get('[data-cy="submit"]').click();

data-cy
data-test
data-testid

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)


  1. iBljad
    15.06.2023 21:59

    Знать xpath все равно стоит для случаев, когда:

    • Локатора ещё нет, а тесты нужны

    • Локаторы есть, но у вас имеется структура элементов вида data-testid=my-list: data-testid=my-item, (где у каждого элемента в списке может быть ещё и data-testid=my-title, и вот по нему и надо найти)


    1. kulykovdmytr Автор
      15.06.2023 21:59
      +1

      Лучше, если сначала фронтенд будет готов для тестов, чем писать тесты и затем исправлять старые локаторы на новые…

      Если у каждого элемента внутри вложенности есть свой тайтл и айтем, это исправляется со стороны фронтенда, в тестовый локатор передается id элемента, и для каждого элемента локатор продолжает быть уникальным.


      1. iBljad
        15.06.2023 21:59
        +2

        1. Конечно лучше быть здоровым и богатым, чем больным и бедным (с), но ждать "готовности" фронта можно долго, т.к. такие доработки обычно не в приоритете, но основная цель тестов -- помогать в обнаружении ошибок/экономить время на регресс, а не быть написанными на века, поэтому оптимальнее их все-таки создать раньше и на том, что есть, а потом обновить локаторы, все-таки их правка занимает меньше 5% от времени на разработку тестов.

        2. Или я не так объяснил, или не так был понят, давайте на примере (искусственном, поэтому без вопросов к логике)): у вас есть страница покупок, где есть два списка: "купить" и "куплено", вам нужно проверить, что итем переехал из одного списка в другой, поэтому просто сверки по тайтлу будет мало, нужно еще учитывать родителя, тут одним лишь test-id не обойтись.


        1. kulykovdmytr Автор
          15.06.2023 21:59

          Проверить появление нового аттрибута у айтема можно силами фреймворка через hasAttribute()/toHaveAttribute(), а ещё, купленный айтем может имет совершенно другой локатор data-testid-bought(itemId)..


          1. iBljad
            15.06.2023 21:59

            Это не новый атрибут, сам элемент не изменяется, меняется дерево -- элемент переезжает, меняя родителя (со списка "купить" на "куплено").

            Про другой локатор -- я специально написал, что пример искусственный (но имеющий аналоги на практике, просто решил не усложнять лишними деталями)


  1. smple
    15.06.2023 21:59
    +4

    просмотрел статью по диаганали, по озвученным проблемам уже давно существует решение в виде page object, отсюда в тестах не используется xpath, а используется page object в котором уже скрыта (инкапсулирована) работа с xpath.

    Итог: не обязательно добавлять data-testid можно использовать обычные id, class, tag если они есть, вопрос именования это отдельная тема, не будем спорить нужен ли БЭМ или альтернативы.

    Добавляя data-testid мы не перестаем использовать xpath, мы вместо использование классов, тэгов, id. Начинаем использовать пользовательские атрибуты.


    1. iBljad
      15.06.2023 21:59
      +2

      Куда бы ни был инкапсулирован локатор, тесты все равно упадут, если произойдут озвученные риски (смена верстки итд, но и с "data-testid" оно может упасть при кардинальных изменениях, если уж на то пошло).


      1. smple
        15.06.2023 21:59

        так кто вам гарантирует что при изменение верстки тот кто верстал не посносил data-testid ?

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

        например пусть была некая колонка с балансом в профиле пользователя, а после редизайна она перехала куда то в другое место, на другую страницу (мой баланс например и там текущий баланс и операции пополнения и расходов) как вы ее будете находить не меняя тестов ? будете оставлять колонку на баланс в профиле с display: none ? (костылище)
        как в этом случае мне поможет подход озвученный вами ?

        тоесть при подобном кейсе в моем варианте.

        1. Разработчик обновляет верстку (редизайн и тд) сфокусировавшись на этом.

        2. Запускает тесты чтобы заметить регресс.

        3. Исправляет регресс у упавших тестов.

        4. 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 селектор») определяет уникальное имя элемента, которое используется для изменения его стиля и обращения к нему через скрипты.

        я даже выделил важное в цитате, как раз то как вы собираетесь использовать его.


        1. smple
          15.06.2023 21:59

          как то я слишком расплывчато написал, попробую кратко по пунктом сумировать

          1. вместо data-testid можно, а возможно даже лучше использовать стандартный id

          2. пользовательские атрибуты (data-*) нужно использовать по назначению там где нет стандартных.

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

          4. использование пользовательских атрибутов это не значит что вы отказались от css селекторов.


          1. kulykovdmytr Автор
            15.06.2023 21:59


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

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

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


          1. up40k
            15.06.2023 21:59

            Page object - хороший, годный слой абстракции. Сделал такой вывод, почитав по диагонали его концепцию в доках Селениума.
            Он не решает озвученную проблему. Да, вы переносите описание локатора непосредственно из теста в какое-то общее место (описание структуры документа), отделяя его от ассертов, но вам всё ещё нужно учитывать нюансы выбранного способа локации.
            Предлагаете доступ к элементам по атрибуту id? Прекрасно. Только разрабу всё равно придётся эти айдишники проставлять для вашего удобства. А потом в проде вас будут нещадно парсить, блочить рекламу и прочее.
            Почему бы не договориться, что в тестовом (предпродовом, если угодно) окружении, где проходят наши тесты, будут использоваться нужные юзер-атрибуты? Да, это нагрузка на разраба, ему нужно будет сверяться с таблицей (которую должен составлять тестировщик, тут отдельная тема того, что эту таблицу нельзя прописать в код page object, но можно описать в документации) и вручную прописывать нужные атрибуты после правки кода и перед пушем. Но тут мы не трогаем id, которые используются в коде приложения и могут быть рандомными. Перед пушем в прод достаточно прогнать скрипт, который убирает из кода добавление юзер-атрибутов.
            Да, то же самое можно с делать с id. И в зависимости от окружения выдавать разные значения этого атрибута (статик в тест, динамик в прод). Я прикинул, в этом просто больше кодинга, а нагрузка на дева такая же (или больше, кодинга же больше).
            > 4
            Да, но глобально - это вопрос унификации взаимодействия. Тестировщика с разработчкиом, тестов с кодом, пользователя с продуктом. Как я написал в каменте ниже, нужно ориентироваться на представление пользователя. И page object, и юзер-атрибуты, и локаторы на основе ARIA вместо селекторов - инструменты, которые мы можем применять в конкретном проекте
            В любом случае, это всё не панацея для выстраивания процесса тестирования. Это может в той или иной степени помочь, и, в зависимости от проекта, быть применимо или нет.


            1. kulykovdmytr Автор
              15.06.2023 21:59

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

              В дальнейшим эти локаторы попадут в PageObject и будут использоваться в тестах.

              Решение с тестовыми аттрибутами используется, причем многими компаниями, мой поинт был отказаться от построения XPath/CSS путей, завязанных на сущности, которые меняются.


        1. iBljad
          15.06.2023 21:59

          так кто вам гарантирует что при изменение верстки тот кто верстал не посносил data-testid 

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

          Чем предложенный вами data-testid отличается от обычного id 

          лично я ничего не предлагал, но у меня есть ответ: в некоторых популярных фронтенд-фреймворках id может быть автогенерируемым (возможно, чтобы избежать использования кеша для старых стилей, но это не точно)

          PS: Глядя на комментарии под статьей, хочу акцентировать, что нигде не писал такие фразы, как "нужно так делать" и "всем 100% подойдет этот подход" :)


          1. kulykovdmytr Автор
            15.06.2023 21:59

            Вы действительно не писали эти фразы, их написал я :)


  1. XaBoK
    15.06.2023 21:59

    А в чём смысл писать "/html/body/div[1]/p/a" или "//a[contains(text(),"example link")]"вместо "//a"?


    1. iBljad
      15.06.2023 21:59
      +1

      потому что на веб-страницах обычно больше одной ссылки и их надо как-то различать?


      1. XaBoK
        15.06.2023 21:59

        И как их различает "body > div > a"?

        Если мы не в вакууме, то тут уже много вариантов: по положению(путь/индекс), по атрибутам, по значению. Хотите как в CSS //body/div/a или//a[@testId="directions"]? Просто тут явно хотели некрасивый и нефункциональный X-Path. Это обесценивает всю статью.


        1. up40k
          15.06.2023 21:59

          Это не обесценивает всю статью, просто примеры как для XPath, так и для CSS-Selectors нерелевантные.


        1. kulykovdmytr Автор
          15.06.2023 21:59

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


  1. 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".


  1. up40k
    15.06.2023 21:59
    +1

    В Selenium из коробки отсутствует поддержка тест-аттрибутов

    Судя по вашим примерам в таблице, основанным на common CSS-селекторах, она отсутствует везде, кроме Playwright.

    Получается, мы просто используем соглашение между фронтом и QA для удобной автоматизации тестирования. Есть серьезный недостаток, который вы не затронули, он касается излишнего автоматизма автоматизации (простите за тавтологию): на фронте может быть поломан UX, но при этом тесты будут проходить. Нам так или иначе нужно отталкиваться от представления пользователя, от того, как он взаимодействует с приложением (страницей, документом).

    В том же Playwright есть локатор page.getByRole(), который призван если не решить, то помочь в решении с представлением пользователя. Само собой, это можно и нужно комбинировать с использованием тестовых атрибутов, если есть такая возможность.


    1. kulykovdmytr Автор
      15.06.2023 21:59

      Плохо представляю, каким образом тесты будут проходить, если поломан UX…
      Если тест написан правильно и в нём есть ассерты, он не сможет пройти..

      getByRole(), кажется, создан для других задач, но через него действительно можно обращаться к аттрибутам, но вопрос - зачем

      await page.getByRole('checkbox', { name: 'Subscribe' }).check();


      1. up40k
        15.06.2023 21:59

        Если тест написан правильно и в нём есть ассерты, он не сможет пройти..

        Сможет. В зависимости от вашего "правильно". Вы заполняете форму, получаете нужный ответ. А пользователь не может. Потому что разраб добавил шифрование пароля, например, поменяв id элементов (используя финты с динамической скрытой формой). Но где-то прошляпил с уровнями иерархии. В итоге ваш тест работает, а в проде не работает.

        кажется, создан для других задач

        Для тех самых, чтобы абстрагироваться от структуры DOM и его атрибутов.

        через него действительно можно обращаться к аттрибутам

        Ну нет же, вы выбираете элемент/набор элементов по ARIA (то, что доступно пользователю), а уже после проверяете атрибуты с помощью фильтров. Где-то там оно должно упасть в случае косяка разраба.


        1. kulykovdmytr Автор
          15.06.2023 21:59

          Вашу проблему подсветит результат сценарного е2е теста: "успешная авторизация и переход в личный кабинет (например)". Здесь я имею в виду не переход в личный кабинет через ссылку или параметр next в query, а как результат абстрактной, успешной авторизации. В тесте проверяется весь путь, дополнительно можно проверить простановку необходимых кук.

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

          getByRole создан для поиска аттрибутов, в том числе созданных неявно. Это решение может подойти для тестовых локаторов с названиями, которые не поддерживаются getByTestId() или если getByTestId() отсутствует в фреймворке.


  1. 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"))


    1. kulykovdmytr Автор
      15.06.2023 21:59

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


  1. antonkrechetov
    15.06.2023 21:59

    Совершенно очевидно, что тесты должны быть привязаны именно к тому, к чему привязан внешний вид/поведение фронтенда. Если у вас обычная верстка по БЭМ и сырой JS типа


    document.querySelectorAll('.button__delete-all-users').addEventListener(...)

    то любые варианты будут хуже селекторов классов. Если же, как в вашем примере, Реакт и material-ui, и вы, вообще говоря, не можете гарантировать какой DOM у вас будет на выходе, то тогда, конечно, дата-атрибуты выгядят неплохим вариантом. Только это костыль, который приходится делать за неимением лучшего.