Как стать автором
Обновить

CSS и XPath — отстой. Секрет стабильных автотестов в test-id

Время на прочтение20 мин
Количество просмотров3.3K

Вступление

Вы вообще в курсе, что такое CSS и XPath селекторы? Ну конечно в курсе — раз уж кликнули на эту статью, наверняка пережили хотя бы один из тех унылых споров в духе «а что лучше: CSS или XPath?» Спойлер: ни то, ни другое. Все эти разговоры — просто шум, рожденный из некомпетентности. Вот эти бесконечные обсуждения — «а XPath может по тексту», «а CSS быстрее», «а вот тут индекс нужен»... Да какая, к чёрту, разница, когда можно просто использовать тестовые идентификаторы?

Серьёзно. Если у элемента есть тестовый идентификатор — всё, точка. Найти его можно быстро, стабильно и без плясок с бубном вокруг селекторов. А выбор между CSS и XPath становится вообще неважным. Это уже вкусовщина, типа «а ты как макароны солишь — до или после закипания воды?». Умным людям не до таких споров.

Возникает логичный вопрос: если всё так круто с тестовыми идентификаторами, почему о них не кричат с каждого угла? Почему в 2025-м году тесты всё ещё ломаются от смены цвета кнопки? Ответ простой: либо люди не знают, либо не умеют. Иногда и то, и другое. В этой статье мы как раз это исправим. Покажем, как правильно и без боли внедрять тестовые идентификаторы в фронтенд‑приложении. Без теории ради теории — только практика и здравый смысл.

И давайте скажем это вслух: если ваши автотесты сыпятся из‑за смены классов, цвета, или позиции элемента — виноваты не разработчики, не продукт, не космос, не ретроградный Меркурий. Виноваты вы. Да‑да, вы, QA Automation инженер, который решил «ну тут же можно взять по div:nth-child(42), норм же работает». Нет, не норм. Вы создали флак, вы допустили нестабильность, вы наплодили технический долг в автотестах. Хотите стабильности? Учитесь писать тесты правильно. Хотите гордиться своими автотестами, а не прятать их отчеты под стол? Добро пожаловать — сейчас научим.

Эта статья — для всех: от автоматизаторов, уставших от флаков, до фронтендеров, которые хотят помочь команде и не видеть, как в pull request прилетает «починил тест» вместо нормального фидбека.

Что круче — CSS или XPath?

Поехали без прелюдий. Вот эти бесконечные дискуссии «а что круче — CSS или XPath?» — это как спорить, чем лучше копать яму: ложкой или вилкой. Забавно? Вот и мне смешно. Потому что оба варианта — костыли, если вы изначально подходите к задаче неправильно.

Ни CSS, ни XPath не являются оптимальными инструментами для UI‑автотестов. Вообще. Забудьте. Правильный, взрослый, стабильный подход — это кастомные атрибуты типа data-test-id. И когда вы их используете, становится абсолютно по барабану, пишете вы CSS или XPath — элемент будет найден быстро, чётко, без танцев и отрыжек селекторной магии. Тут уже дело не в типе селектора, а в том, на что вы этот селектор навешиваете.

Если вы всё ещё цепляетесь за текст, классы, nth-child, динамически сгенерированные ID, то, простите, не нужно потом жаловаться, что у вас «тесты флакают», «после рефакторинга всё упало» или «а что случилось, я ничего не менял». Сюрприз: вы сами себе враг. Вы выбрали путь страдания.

Возьмите мобильную автоматизацию, например. Там никто даже не думает писать тесты без тестовых идентификаторов. Просто потому, что нельзя. А в вебе исторически можно схалтурить: закинуть на скорую руку CSS или XPath локаторы и вроде как работает. Ключевое слово — «вроде». Работает — пока не перестаёт. А потом боль, костыли, и попытка понять, почему из‑за смены названия класса у вас теперь валится весь регресс.

И чем дальше, тем хуже. Проект обрастает десятками «временно рабочих» CSS/XPath локаторов, и в какой‑то момент вы уже не можете их нормально отрефакторить. Потому что, внимание, тесты становятся заложниками качества и стабильности селекторов. В итоге даже самый простой тест превращается в квест с подземельями, багами и слезами автоматизатора.

CSS и XPath: как НЕ надо

Я не буду тратить ваше и своё время на бессмысленное «сравнение плюсов и минусов» CSS и XPath. Это всё разговоры ради разговоров. Хотите правду? Они оба — не то, чем вы должны пользоваться. Вместо цирка про «что быстрее», давайте честно пройдёмся по главным болячкам этих подходов. Спойлер: всё плохо.

1. Завязка на текст: «А где кнопка?»

//button[text()='Отправить']

О, классика! Работает… до первой правки текста. А потом:

  • дизайнер захотел вместо «Отправить» — «Отослать»

  • UX‑специалист вспомнил про «Повышение конверсии»

  • продакт‑менеджер включил английский язык

  • а вы — включили панику

Итог: тесты отвалились. Причём интерфейс-то не поменялся. Только слова. А вы всё ещё ищете кнопку по её бывшему имени, как бывшую — по старому нику в Telegram.

2. Глубокая вложенность: «Интерфейс немного изменился — тесты умерли»

div.container > div.section > div.actions > button.primary

Красота? Нет. Хрупкий кусок боли. Вставили лишний div? Удалили лишний блок? Изменили порядок? Поздравляю, вы снова чините автотест, который не обязан был сломаться.

3. Классы и стили: «Они же стабильны… ага, конечно»

button.primary.large

Никогда не забывайте: классы не для тестов, они для внешнего вида. Они меняются. Часто. Не потому что кнопка исчезла, а потому что large больше никому не нравится. А вы снова страдаете. Потому что решили строить автотесты на песке.

4. Случайные ID: «Каждый билд как русская рулетка»

#react-17gh2j93-btn

Сначала кажется: «О, id — уникально, надёжно!» Ага. Только вот React, Vue и другие милые ребята генерируют их каждый раз заново. Сюрприз: селектор работает один раз. Потом — идите курить логи.

5. Читаемость: «Что это вообще такое?»

div:nth-child(3) > button.btn-lg.primary

Даже если этот локатор работает — понять, что он делает, невозможно.

А теперь посмотрите на это:

[data-test-id="create-course-button"]

Видите разницу? Один селектор — как шифр из «Игры в кальмара». Другой — понятен даже вашему проджекту.

Что такое тестовые идентификаторы?

Всё просто: тестовые идентификаторы — это специальные атрибуты в HTML, которые нужны не для фронтенда, не для верстки, а исключительно для автотестов.

Они не влияют на внешний вид, поведение или UX. Их задача — быть якорем. Стабильным, читаемым и предсказуемым.

<button data-test-id="submit-button">Отправить</button>

Как их называть? Да как хотите:

  • data-test-id

  • data-testid

  • qa-id

  • test-id

  • data-qa

  • хоть data-holy-button

Главное — чтобы было:

  • однозначно (один идентификатор — один элемент)

  • стабильно (не зависит от текста, стилей, порядков)

  • понятно (без m7fg9btn99 и abc123)

Некоторые фреймворки даже подсказывают «дефолтные» названия. Например, Playwright из коробки умеет работать с data-testid. Но и это не догма — в конце концов, вы не роботы. Вы инженеры. Назовите как угодно — главное, начните использовать.

Почему все про них знают, но почти никто не использует?

Потому что, чёрт возьми, это не халява. Нужно:

  • понять, как работает фронт

  • договориться с командой

  • договориться с самимсобой, что «быстро и криво» — это путь в ад

А ещё потому что проще натыкать 20 XPath'ов и свалить с работы пораньше. Только потом те же 20 XPath'ов разнесёт первым же редизайном. А виноват кто? Правильно — вы.

Это как раз и отличает настоящего QA Automation‑инженера от человека, который «просто пишет тесты» — желание строить фундамент, а не временные палатки.

Зачем это вообще?

Вставлять data-test-id в интерфейс — это не «ещё одна фича», это фундамент надёжной автоматизации UI‑тестов. Если вам нужны стабильные, предсказуемые и легко поддерживаемые тесты — это не рекомендация, это must-have.

Вот зачем это работает и почему без него — боль:

1. Стабильность и предсказуемость

CSS/XPath-подходы — как игра в рулетку: чуть поменяли DOM или порядок элементов — всё, локатор умер. А data-test-id живёт, пока вы явно не убрали элемент. Падает только по делу, а не из-за новой обёртки в div.

2. Не зависят от внешнего вида

Классы меняются — стили меняются — верстальщик захотел «посвежее» — CSS/XPath‑селекторы снова мертвы. Атрибут data-test-id изолирован от всего этого. Он не для дизайна. Он для тестов. Его не трогают «просто так».

3. Устойчивость к рефакторингу

Если фронтендер переносит кнопку в другой компонент, но оставляет data-test-id — тест даже не заметит изменений. Без него — «404 Not Found», даже если кнопка визуально на месте.

4. Улучшает коммуникацию

Когда разработчиквидит в DOM data-test-id, он сразу понимает: «Окей, это тестовый якорь, трогаю аккуратно или обсуждаю с QA». Это культура. Это осознанность. Это уменьшает количество внезапных «почему всё упало?!».

5. Чистота и читаемость

Что выглядит понятнее?

div:nth-child(4) > .card .actions > button.btn.primary

против:

[data-test-id="delete-course-button"]

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

6. Экономия времени на дистанции

Да, на старте CSS/XPath кажутся быстрее. Но дальше — начинается ад: правка верстки → 15 упавших тестов → день на фикс.

С data-test-id:

  • тесты не разваливаются

  • не приходится «охотиться» за элементами заново

  • не нужна реанимация локаторов после каждого спринта

Это инвестиция, которая окупается с первых недель и сохраняет нервы всей команде.

Безопасно ли это?

Ах да, классический довод: «Не‑не‑не, не трогай DOM! Это сломает рендеринг, просадит перформанс и ухудшит UX
Перевод: »Я просто не хочу, чтобы QA лез в мой священный фронтенд.»

Ну давайте разложим по фактам. Потому что истина тут простая, как <div> без class.

1. «Сломает стили»

Как?! Это обычный data- атрибут. Он не влияет ни на CSS, ни на JS, ни на цвета кнопки в Вальгалле. Если у вас стили завязаны на data-test-id, то… ну тут даже тесты не помогут.

2. «Просадит перформанс»

Серьёзно? Атрибут весом в 15 байт замедлит ваше React-приложение? Вы там случайно не через useEffect рисуете галактику? Измеряем в наносекундах, а обсуждаем как будто у вас сервер под нагрузкой рухнет от одного data-test-id.

3. «Ухудшит UX»

А, ну конечно, пользователь такой открывает страницу… и испытывает острый приступ UX-дискомфорта от атрибута, который он даже не видит. Зато мы все знаем: если у UX что-то не так — это точно data-test-id виноват, а не логика или контент.

4. «Это видно поисковикам! SEO пострадает!»

Во-первых, не пострадает. Во-вторых, если очень хочется — можно даже сделать умный шаблон, где data-test-idaria-label, и будет только лучше. Так что это уже не про SEO, а про отмазки.

5. А что на самом деле?

  • Не влияет на стили

  • Не влияет на производительность

  • Не отображается пользователям

  • Не мешает разработке

  • Не ломает компоненты

  • Не вызывает глобальное потепление

Это спокойный, неагрессивный, предельно предсказуемый способ дать автотестам якорь. И если ваш фреймворк умеет в HTML — значит он точно умеет в data-test-id.

React, Vue, Angular, SolidJS, Svelte, Web Components, даже чистый HTML с ванильным JS —
везде работает. Везде безопасно.

Если разработчик против data-test-id — скорее всего, он просто не хочет, чтобы вы могли зацепиться за его DOM. Но это уже не про производительность. Это про территорию. И да, она теперь и ваша тоже.

Как расставить тестовые идентификаторы?

Окей, пора перейти от слов к делу. Как именно ставить эти ваши data-test-id, если вы, скажем, не фронтенд-разработчик, а просто честный автоматизатор с хорошими намерениями?

На первый взгляд может показаться, что это что-то из разряда «требуется знание React на уровне Senior++», но на деле — это базовый скилл, как открыть DevTools или запустить тест.

Правда жизни

Каждый день тысячи QA Automation инженеров по всему миру просто открывают HTML, вставляют data-test-id="что-то-понятное" — и всё работает. Даже в мобильной разработке (да-да, с этими вашими Flutter, Jetpack Compose и SwiftUI), где всё как будто из магии и анимаций, идентификаторы всё равно ставят. Это уже не «хак», это индустриальный стандарт.

Шаг 1. Установка Node.js

Да, давайте начнём с азов. Если вы собираетесь трогать хоть один байт фронтенд-кода — вам нужен Node.js. Почему?

Потому что:

  • Вы разрабатываете фронтенд? → Нужен Node.js

  • Вы собираете фронтенд? → Нужен Node.js

  • Вы просто хотите поставить себе линтер? → Сюрприз: Node.js

  • Вы хотите посмотреть что-то в UI? → Тоже, мать его, Node.js

Node.js — это платформа, которая позволяет запускать JavaScript вне браузера. И всё, что связано с современными фронтендами (React, Vue, Angular, Svelte, SolidJS, Astro, you name it) — работает через него.

Как установить Node.js?

Не буду пересказывать документацию (она шикарна, кстати). Просто заходите сюда: https://nodejs.org/en/download. Выбираете свою ОС, кликаете мышкой — и всё. Node.js сам всё поставит, и бонусом даст вам npm — пакетный менеджер.

Шаг 2. Установка проекта

Ну что, пора, наконец, залезть в проект. Потому что прежде чем что-то в нём менять — будь то стили, компоненты или наши драгоценные data-test-id — надо его хотя бы скачать. Да-да, просто «что‑то поменять на проде» не получится. Сначала к себе на комп, пожалуйста.

Клонируем проект

Для примера будем использовать учебный проект: https://github.com/Nikita-Filonov/qa-automation-engineer-ui-course. Клонируем его как нормальные люди:

git clone https://github.com/Nikita-Filonov/qa-automation-engineer-ui-course.git
cd qa-automation-engineer-ui-course

Установка зависимостей

Перед тем как нажимать на кнопочки и запускать фронт, надо сначала оживить проект. Потому что пока это просто папка с непонятным node_modules в надежде, что они появятся по магии. Не появятся. Устанавливаем зависимости.

Используем правильный пакетный менеджер. Тут главное не тупить. У проекта может быть yarn или npm. Как определить?

  • Если в корне есть файл yarn.lock → значит, используем yarn

  • Если там package-lock.json → значит, используем npm

Примеры:

Для yarn:

yarn install

Для npm:

npm install

И не надо мешать npm и yarn в одном проекте. Это не коктейль. Это грех.

Ждём немного... Да, установка может занять минутку. Иногда — три. Иногда — вечность, если у вас Wi-Fi в стиле «ловлю соседа на лестничной клетке». Но это нормально. Главное — после установки у вас появится волшебная папка node_modules, и проект оживёт.

Запускаем проект

И вот тут начинается «веселье». Возможно, вы ожидали какую‑то одну универсальную команду вроде npm start, и она сработает. Иногда. Если звёзды сойдутся. Но чаще всего:

  • В проекте используется нестандартная структура

  • У команды совсем другое имя (например, app:start, dev, run, frontend:start)

  • Включён TypeScript, монорепа или ещё какое-нибудь веселье

Что делать? Открываем package.json. Это священный грааль для любого Node.js-проекта. В нём есть блок scripts — и именно там указаны все команды, которые можно запускать. Вот как он может выглядеть:

"scripts": {
  "start": "react-scripts start",
  "app:start": "vite --host",
  "build": "vite build"
}

Если не знаете, что запускать — не страдайте, просто спросите у фронтенд-разработчиков. Серьёзно, они не кусаются (если спросить вежливо). Это лучше, чем устраивать шаманские танцы вокруг терминала.

Для нашего проекта запускаем команду:

yarn start

После этого у вас локально поднимется фронтенд-приложение по адресу: http://localhost:3000. Вот теперь у вас есть живая страница, которую можно щупать, трогать, ломать, ломать ещё раз — и, конечно же, ставить на неё data-test-id как боженька тестирования.

Шаг 3. Учимся расставлять тестовые идентификаторы

Что такое JSX?

JSX (JavaScript XML) — это такая хитрая смесь HTML и JavaScript, которую придумали для React. Выглядит как HTML, но внутри работает как настоящий JavaScript. Прямо как бургер с сюрпризом: сверху булочка — вроде знакомо, а внутри... JavaScript.

Пример:

<button>Нажми меня</button>

На вид — обычная кнопка. Но это уже не HTML, а JSX. Его фишка в том, что вы можете писать такие элементы прямо внутри JavaScript-файлов, и React будет знать, что с ними делать.

Отличия JSX от HTML

JSX похож на HTML, но не вздумайте писать как в старой школе, есть нюансы:

  1. class нельзя. Вместо class="btn" пишем className="btn", потому что class — это уже ключевое слово в JavaScript, и он вас просто пошлёт компиляться с ошибкой.

  2. Обработчики событий = camelCase. Вместо onclick="..." пишем onClick={...}. Большая C — это не ошибка, это стиль JSX. Привыкайте.

  3. Один родитель сверху. JSX не может вернуть два соседних тега. Всё должно быть в одном корневом контейнере:

    Так нельзя:

    <h1>Привет</h1>
    <p>Мир</p>

    А так можно:

    <div>
      <h1>Привет</h1>
      <p>Мир</p>
    </div>
  4. Все теги должны быть закрыты. Даже <input>! Забудьте про HTML-расслабон.

    <input />   // правильно
    <input>     // ошибка

Что такое компонент?

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

Пример функционального компонента:

export const LoginButton = () => {
  return <button data-test-id="login-page-submit-button">Войти</button>;
};

Вот и всё. Это уже компонент. Можно вставлять его в любом месте вот так:

<LoginButton />

React сам вставит кнопку с нужным data-test-id, и ваш автотест её найдет за миллисекунду.

Компоненты бывают:

  • Функциональные (используются в 95% случаев) — как выше.

  • Классовые — это старьё. Если вы их видите — бегите или зовите на помощь. Скорее всего, вам они не понадобятся.

Где указывать data-test-id?

Просто: на том JSX-элементе, к которому должен обратиться автотест. Вот и вся философия. Пример:

<input
  type="text"
  placeholder="Введите email"
  data-test-id="subscribe-form-email-input"
/>

Никакой магии. Теперь автотест найдёт этот input и будет знать, что именно он — поле для email.

Как найти нужный элемент в коде?

Вы открыли фронт и видите: «Ага, вот тут есть кнопка 'Отправить', а вот заголовок 'Welcome to UI Course application!'». Но возникает логичный вопрос: «А где это в коде, мать его?». Именно туда и надо добавить data-test-id, но сначала это нужно найти.

Ниже — три проверенных способа, которые реально работают и экономят кучу времени.

Способ 1: Поиск по видимому тексту

Прямолинейно, быстро, эффективно. Берете текст с экрана, прокидываете его в поиск по проекту и — бам! — находите нужный компонент.

Вы видите в браузере:

<h5>Welcome to UI Course application!</h5>

Берете "Welcome to UI Course application!", вбиваете в глобальный поиск (например, в VSCode), и если текст захардкожен, вы сразу окажетесь в JSX-файле, где он написан.

Плюсы:

  • Быстро.

  • Не требует танцев с бубном.

  • Идеально работает в простых проектах.

Минусы:

  • Не сработает, если используется локализация (i18n) — там текстов в JSX просто нет.

Способ 2: Поиск по ключу локализации (если проект на i18n)

Если проект многоязычный, то видимого текста в коде вы не найдёте. Вместо этого будет что-то вроде:

<h5>{t("ui_course_welcome_page_title")}</h5>

Такой код берёт текст из отдельного файла с переводами. Ваша задача:

  1. Посмотрите текст в браузере.

  2. Найдите его в одном из .json или .ts файлов локализации.

  3. Посмотрите, какой у него ключ (ui_course_welcome_page_title).

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

Плюсы:

  • Работает в 100% i18n проектов.

  • Даёт точный ответ — в каком компоненте вы находитесь.

Минусы:

  • Надо понимать, как устроены файлы локализации.

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

Способ 3: Просто спросить у фронтендера

Да, это самый очевидный, но почему-то забываемый способ. Подходите (или пишите в чат) и говорите:

“Бро, где живёт кнопка 'Отправить'? Мне туда data-test-id воткнуть надо.”

И вуаля — разработчик:

  • Говорит, как называется компонент (WelcomePage, LoginForm, SubmitButton, что угодно);

  • Присылает путь к файлу;

  • Может даже сам навесить data-test-id по доброте душевной.

Почему это работает:

  • Разработчик знает структуру лучше всех.

  • Экономит вам часы бестолкового блуждания.

  • Заодно улучшите коммуникацию внутри команды.

Лайфхак: Если вы QA в команде — договоритесь с фронтендерами, чтобы сразу ставили data-test-id при создании компонентов. Всё равно потом ставить придётся — лучше сразу.

Как правильно расставлять data-test-id: практические кейсы

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

Случай 1: Простой компонент — просто добавляем data-test-id

Если вы контролируете компонент и элемент доступен напрямую — добавляете идентификатор прямо в JSX. Никаких сложностей.

Пример:

export const WelcomeView = () => {
  return (
    <div>
      <h5 data-test-id={'welcome-title'}>Welcome to UI Course application!</h5>
    </div>
  );
};

Что происходит:

  • У вас есть заголовок с текстом.

  • Вы добавляете data-test-id="welcome-title" — и теперь автотест легко найдёт этот элемент.

  • Название делаем по шаблону: {контекст}-{тип_элемента}. Например: welcome-title.

Когда так делать:

  • Элемент находится в вашем коде, а не во вложенном компоненте.

  • Компонент простой, без магии.

  • Вы контролируете разметку.

Случай 2: Вложенные компоненты — нужно идти внутрь

Когда нужный элемент внутри другого компонента — просто так data-test-id не добавить. Нужно открыть вложенный компонент и разметить его там.

Пример:

export const FormView = () => {
  return (
    <div>
      <label>Email</label>
      <input data-test-id={'form-email-input'} />
      <button data-test-id={'form-login-button'}>Login</button>
    </div>
  );
};

export const WelcomeView = () => {
  return (
    <div>
      <h5 data-test-id={'welcome-title'}>Welcome to UI Course application!</h5>
      <FormView />
    </div>
  );
};

Что происходит:

  • В WelcomeView есть компонент FormView.

  • Но input и button живут внутри FormView.

  • Мы не размечаем их снаружи, а добавляем data-test-id внутри самого FormView.

Когда так делать:

  • Элемент спрятан во вложенном компоненте.

  • Вы можете открыть компонент и изменить его.

  • Важно сохранить локальный контекст — каждый компонент отвечает за свои data-test-id.

Случай 3: Переиспользуемые компоненты — передаём testId через props

Если у вас компонент, который юзается в разных местах — хардкодить data-test-id в нём нельзя. Он потеряет уникальность. Нужно передавать часть ID снаружи, как параметр.

Пример:

type ListItemProps = {
  title: string;
  description: string;
  testId?: string;
};

export const ListItem: FC<ListItemProps> = ({ title, description, testId }) => {
  return (
    <div data-test-id={`${testId}-list-item-container`}>
      <h6 data-test-id={`${testId}-list-item-title`}>{title}</h6>
      <p data-test-id={`${testId}-list-item-description`}>{description}</p>
    </div>
  );
};

export const WelcomeView = () => {
  return (
    <div>
      <h5 data-test-id={'welcome-title'}>Welcome to UI Course application!</h5>
      <ListItem testId={'playwright'} title={'Playwright'} description={'For Python'} />
      <ListItem testId={'pydantic'} title={'Pydantic'} description={'For Python'} />
      <ListItem testId={'httpx'} title={'HTTPX'} description={'For Python'} />
    </div>
  );
};

Что происходит:

  • Компонент ListItem используется несколько раз.

  • В каждом вызове мы передаём уникальный testId.

  • Внутри компонента формируются уникальные data-test-id — вроде playwright-list-item-title.

Что такое props:

  • Это просто входные параметры компонента.

  • Через них мы можем гибко конфигурировать поведение и разметку.

  • В нашем случае — передаём testId.

Когда так делать:

  • Компонент универсальный и используется в разных контекстах.

  • Нужно сохранить уникальность data-test-id.

  • У компонента нет контекста сам по себе, его дают извне.

Пример с индексами

Если рендерится список, и у вас нет уникального ID — можно использовать индекс:

{items.map((item, index) => (
  <ListItem
    key={item.id}
    testId={`course-list-${index}`}
    title={item.title}
    description={item.description}
  />
))}

Но! Если есть item.id — лучше использовать его, а не индекс: testId={course-${item.id}}

Почему?

  • Индекс может меняться при изменении списка.

  • ID — стабильнее, надёжнее, точнее.

Ставим свой первый тестовый идентификатор

Окей, мы разобрались с базой. Теперь пора поставить свой первый data-test-id. В тестовом приложении, которое мы устанавливали ранее, есть специальная страница:
http://localhost:3000/#/welcome — и на ней пока не расставлены data-test-id.

Давайте попробуем сделать это пошагово.

Шаг 1: Найти нужный элемент

На странице /welcome вы увидите крупный заголовок: Welcome to UI Course application!

Он расположен в самом верху, внутри компонента с белым фоном (обёрнут в Paper) и визуально является главным заголовком страницы.

Шаг 2: Найти компонент в коде

Этот заголовок находится в компоненте WelcomeView, который расположен по пути: /src/Views/Welcome/WelcomeView.tsx. Фрагмент кода до изменений:

import { BasePaper } from '../../Components/Views/BasePaper';
import { Grid2, List, Typography } from '@mui/material';
import { WelcomeTopicListItem } from '../../Components/ListItems/Welcome/WelcomeTopicListItem';

export const WelcomeView = () => {
  return (
    <BasePaper sx={{ mt: 3 }}>
      <Grid2 container spacing={2}>
        <Grid2 sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} size={{ md: 5, xs: 12 }}>
          <Typography variant={'h3'}>
            Welcome to UI Course application!
          </Typography>
        </Grid2>
        <Grid2 size={{ md: 7, xs: 12 }}>
          <Typography variant="h6">What you&#39;ll learn:</Typography>
          <List dense>
            <WelcomeTopicListItem title={'Playwright'} />
            <WelcomeTopicListItem title={'PageObject, PageComponent, PageFactory'} />
            <WelcomeTopicListItem title={'UI Coverage Tool'} />
            <WelcomeTopicListItem title={'CI/CD'} />
            <WelcomeTopicListItem title={'Strategy of data-test-id'} />
            <WelcomeTopicListItem title={'Allure'} />
            <WelcomeTopicListItem title={'And more!'} />
          </List>
        </Grid2>
      </Grid2>
    </BasePaper>
  );
};

Открываем этот файл в редакторе и видим знакомую структуру JSX.

Шаг 3: Добавить data-test-id

Теперь добавим data-test-id к элементу <Typography>, который содержит заголовок.

Было:

<Typography variant={'h3'}>
  Welcome to UI Course application!
</Typography>

Стало:

<Typography variant={'h3'} data-test-id={'welcome-view-main-title'}>
  Welcome to UI Course application!
</Typography>

Почему welcome-view-main-title?

Разберём идентификатор по частям:

  • welcome-view — контекст, в котором находится элемент.

  • main-title — тип и смысл элемента: это основной заголовок страницы.

Такой шаблон делает data-test-id:

  • читаемым,

  • понятным,

  • уникальным в пределах проекта.

Это лучше, чем просто main-title или title, так как изолирует идентификатор в контексте WelcomeView.

Шаг 4: Проверить в DOM

Сохраняем изменения, обновляем страницу в браузере и открываем инструменты разработчика (DevTools). Ищем элемент с data-test-id="welcome-view-main-title":

Если вы его видите — значит всё сделано правильно.

Вывод

Как видите, поставить data-test-id — это просто. В большинстве случаев вам нужно:

  1. Найти нужный элемент в интерфейсе.

  2. Найти соответствующий компонент в коде.

  3. Добавить data-test-id, опираясь на контекст и роль элемента.

Со временем это станет автоматическим навыком, который занимает буквально пару секунд.

Как обстоят дела на самом деле в реальных проектах?

Реальность: data-test-id? Ха! Его почти никогда нет

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

Почему? Потому что (внимание, шок-контент) продукт делают для пользователей, а не для ваших автотестов. Вот так поворот.

Так что если вы видите чистый, идентификаторно-девственный DOM — это не баг, это фича.

Что с этим делать? Брать ситуацию в свои руки

Да, вам, скорее всего, придётся инициировать обсуждение. Да, возможно, даже сделать презентацию. Но кто, если не вы?

Вот что реально работает:

  • Проведите 15-минутный митинг. Без скучного буллшита, только боль и правда.

  • Покажите разваливающиеся локаторы на CSS или XPath. Чем больше — тем больнее.

  • Объясните,сколько часов жизни уходит на «починил → снова сломалось».

  • Скажите магическую фразу: «Мы будем чинить старьё вместо того, чтобы покрывать новое. Это не автотесты, это технический долг с таймером.»

Уверяю, после этих слов хотя бы один человек в зале начнёт молча гуглить data-test-id best practices.

Как внедрять data-test-id и не развалить проект?

Спокойно. Никто не говорит "стопаем разработку и начинаем добавлять ID-шники". Всё можно делать аккуратно и параллельно:

  • Пока нет автотестов — вы всё равно делаете ручное тестирование.

  • Потратьте 1–2 часа в день на расстановку ID в ключевых местах.

  • Когда основные зоны покрыты — начинайте писать тесты.

  • Самые жирные точки входа — формы, списки, кнопки. Туда — в первую очередь.

Подключайте фронтенд-разработчиков (сюрприз: они не против)

Серьёзно, для фронтенда поставить data-test-id — это даже не задача. Это полсекунды внимания.

  • Не требует архитектуры.

  • Не трогает бизнес-логику.

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

  • Идеально живёт рядом с рефакторингом.

Хотите, чтобы они помогали? Дайте им гайд на одну страницу. Или бросьте ссылку в Confluence. Всё.

Важно понимать

data-test-id — не панацея. Тесты всё равно будут падать: баги, переделки, жизнь. Но! Вы минимизируете 95% случайных падений, которые происходят потому что:

  • поменяли текст,

  • стиль ушёл на три пикселя влево,

  • элемент телепортировался в другой угол DOM.

Это не спасёт от апокалипсиса, но спасёт от тысячи мелких проблем.

Вывод

Хотите стабильные автотесты — возьмите data-test-id под контроль. Не ждите, пока кто-то вспомнит о тестах в пятницу вечером перед релизом. Сделайте это сами, сделайте красиво.

Общий принцип именования data-test-id

Вы, конечно, можете назвать кнопку btn123 или elementX. А можете — как человек, который уважает своих будущих коллег (и себя через неделю).

Базовый шаблон:

{context}-{element-type}-{index или id (если нужно)}

Компонент

Пример

context

login-page, course-view, navbar

element-type

title, button, input, link

index / id

0, 1, user-42, item-3

Главное правило: уникальность в рамках страницы. Не устраивайте "битву кнопок" с одинаковыми ID.

Шаблоны и примеры

Тип элемента

Шаблон

Примеры

Заголовки (h1–h6)

{context}-title

login-page-title, dashboard-title

Параграфы (<p>)

{context}-text

user-bio-text, offer-description-text

Кнопки

{context}-button

login-submit-button, cart-clear-button

Ссылки

{context}-link

navbar-home-link, support-link

Поля ввода

{context}-input

login-email-input, search-input

Textarea

{context}-textarea

feedback-textarea, course-desc-textarea

Чекбоксы

{context}-checkbox

terms-accept-checkbox, filter-active-checkbox

Радио-кнопки

{context}-radio

gender-male-radio, delivery-standard-radio

Выпадающие списки

{context}-select

country-select, lang-select

Элементы списка

{context}-list-item-{index}

faq-list-item-0, topic-list-item-3

Табличные строки

{context}-row-{id}

user-table-row-123, product-table-row-5

Карточки / контейнеры

{context}-(card/container)-{id}

lesson-card-42, checkout-container

Изображения

{context}-image

profile-image, banner-image

Рекомендации

  • Используйте kebab-case (через дефис), а не camelCase или, прости господи, PascalCase.

  • Делайте ID осмысленным. login-page-submit-button гораздо понятнее, чем btn5.

  • Не пишите ерунду вроде button-button или input-input-field — вы не робот.

  • Для повторяющихся элементов всегда указывайте индекс или ID — это спасёт от ада при кликах по спискам.

Запомните мантру: Контекст + Суть + Уникальность = Надёжный data-test-id. Потом скажете себе спасибо. Или хотя бы не будете себя проклинать на ретро.

Пример: как Playwright дружит с data-test-id

Если вы используете Playwright — поздравляю, вы в хорошей компании. И вот приятная новость: он по умолчанию заточен под работу с data-testid. Но если у вас в проекте решили изобрести своё — типа data-qa-id, data-pw, qa-id или test-id — не проблема.

Вот как сделать всё красиво:

from playwright.sync_api import sync_playwright

with sync_playwright() as playwright:
    browser = playwright.chromium.launch()
    page = browser.new_page()

    # Говорим Playwright: "мы не такие, у нас свой data-атрибут"
    playwright.selectors.set_test_id_attribute("data-qa-id")

    page.goto("https://example.com")

    # Теперь можно писать чистые и надёжные локаторы
    login_button = page.get_by_test_id("login-button")
    login_button.click()

Магия в том, что get_by_test_id(...) теперь будет искать по data-qa-id="...", а не data-testid.

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

  • data-test-id

  • data-qa

  • data-id

  • qa-id

  • data-pw

    Главное — задать атрибут один раз, и пользоваться как человек.

Зачем вообще это нужно?

Чтобы не городить XPath на полэкрана или кликать по «второму div, где третий span, у которого сосед справа с классом .active». Вместо этого:

page.get_by_test_id("checkout-submit-button")

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

Заключение

Если вы всё ещё думаете, что data-test-id — это какая‑то «штука для автоматизаторов», давайте скажу прямо: это часть инженерной культуры. Такой же как читаемый код, линтер, или адекватные названия переменных.

Писать автотесты без data-test-id — это как строить небоскрёб без лифта: можно, но по лестнице долго и устаёшь.

И помните:

  • data-test-id — это не костыль, а инструмент предсказуемости.

  • Это не «сделка с совестью», а договор между тестами и интерфейсом.

  • Это не про «удобно тестерам», а про меньше багов в проде, меньше хаоса в команде.

CSS и XPath — это прошлое. А data-test-id — это то, что делает автотесты надёжными, предсказуемыми и стабильными. Хотите вы этого или нет — он победит.

Теги:
Хабы:
+43
Комментарии25

Публикации

Работа

Ближайшие события