|
Professor Seleznov
|
 Привет, меня зовут Алексей и я C# разработчик. Однажды передо мной стояла задача написать утилиту для взаимодействия с различными UI-элементами в Windows и во всех популярных браузерах. Сама утилита не была связана с тестированием, но вполне годилась для автоматизации некоторых действий на машине, так как была простой в управлении и интуитивно понятной. Мне понравилось работать в этом направлении и возникла идея создания инструмента, который не будет перегружен широким функционалом RPA решений, но возьмёт от них всё что нужно для тестирования интерфейсов, чтобы получился действительно полезный инструмент-помощник для QA с низким порогом входа. Назвал я его RTHelper. Я хочу рассказать в этой статье о нескольких вещах:
- как превратить элемент интерфейса в структуру, по которой мы его потом однозначно найдём
- как влиять на такие элементы, не используя привычные средства ввода
- как выстраивать стабильную цепочку действий с UI, которая не падает если элемент переместился или грузился на 3 секунды дольше
- как быть если интерфейс переменчив и нам нужно получать подробный отчёт об этих изменениях
И, само собой, я покажу какие инструменты я хочу предложить тестировщикам и всем кто имеет дело с какой-либо однообразной рутиной пользуясь компьютером. Введение В UI-тестировании есть неприятная зона между ручной проверкой и полноценной автоматизацией. Руками делать долго и скучно, а полноценная автоматизация всё ещё требует кода, времени и поддержки. Особенно в регрессах где нужно просто повторять типовые действия: открыть окно, нажать кнопки, ввести данные, проверить текст, снять результат. Я хотел закрыть именно этот промежуток: сделать инструмент, который позволяет собирать и воспроизводить UI-сценарии без кода, но при этом не превращается в тяжёлую RPA-систему. По сравнению с фреймворками типа Playwright под управлением ИИ, визуальный конструктор с полным инструментарием под рукой должен стать удобнее когда сценарий или интерфейс сложные и алгоритм надо поддерживать, особенно когда надо править и пробовать локаторы. Мой путь начинался с создания утилиты которая сможет просто собирать дерево элементов из разных приложений и показывать их свойства. Для Windows таких существует много и с помощью них я узнавал какую информацию об элементе мы вообще можем получить и какими средствами она добывается. Для браузеров всё немного сложнее, я перебрал несколько вариантов и остановился на написании собственного расширения, но об этом позже.

Стандартный inspect.exe - пример того как строится UI дерево и какими свойствами обладают элементы Далее, развивая функционал, я добавил в свою утилиту возможности для взаимодействия с сохранёнными элементами: клик, скролл, drag and drop, что угодно… А также работа с сущностями самих приложений: запуск с параметрами, свернуть, эмуляция ввода с клавиатуры… Сама по себе такая программа не даст нам ничего кроме прокси-кликов в кнопки и возможности поглазеть на список атрибутов этих кнопок (класс, айди, координаты и т.д.), но если немного подумать, то мы обретаем хорошую базу для управления компьютером, а в частности тестируемыми интерфейсами. Чтобы решение выросло до более менее серьёзного уровня мне ещё кое-чего не хватало:
- Проигрыватель алгоритмов, состоящих из шагов типа “элемент+действие”
- Набор действий направленных на стабилизацию алгоритма (Wait, скриншот-проверки и другое)
- Инструмент для анализа результатов запуска с подробным отчётом
- Ну и конечно соответствующий интерфейс с редактором параметров, деревом алгоритма и т.д.
Очевидно этого недостаточно для комфортного использования и уж тем более “низкого порога входа”, но это необходимый минимум, который должен быть построен. Со временем всё это обросло большим количеством функционала и фишек для быстрой и простой разработки алгоритмов, которые помогают придерживаться хорошей практики при создании автотестов. Но интереснее в первую очередь рассказать вам про то как работает сердце, а точнее руки программы - библиотека для взаимодействия с UI-элементами. Работа с UI-элементами изнутри Я решил начать с простого и реализовать два режима работы с UI: windows и web. Другие платформы пока в планах на развитие, а что касается этих, то я пришёл к тому чтобы использовать такие подходы:
- Для windows - UIAutomation, можно сказать целевое API для взаимодействия с UI от майкрософт, наследник MSAA
- Для web - после долгих проб и ошибок я выбрал написать собственное браузерное расширение с целью иметь полную власть над веб-интерфейсами
Оба выбора обусловлены, в том числе, следующими требованиями:
- Максимально широкий список доступных действий с элементами, как в больших RPA-решениях
- Возможность захватить элемент под мышью и получить его типовой описатель, по которому мы сможем быстро и однозначно его найти
- Возможность находить элементы по набору атрибутов который задаст пользователь
- Возможности по управлению самим приложением: запуск, ресайз, переключение вкладок, закрытие и прочее
- И конечно же надёжность и стабильность, потому что сценарий тестирования не должен падать по вине самой утилиты
Работа с UIAutomation (UIA режим) UIAutomation - это библиотека, на которой построены практически все решения и фреймворки в области автоматизации UI для windows. Много документации, работать с ней несложно, особенно если вникнуть в то как вообще строится и затем обходится дерево элементов в интерфейсе, однако магии ждать от неё тоже не стоит. Мы не можем исключить следующие сложности:
- Отсутствие у элемента надёжных атрибутов для идентификации
- Наличие множества элементов с одинаковыми атрибутами
- Множество окон у одного приложения, имеющих одинаковые свойства и имена процесса
- Устаревшие интерфейсы (например на c++/delphi), в которых интерфейс иногда строится вообще непредсказуемо
- Элемент попросту отсутствует в интерфейсе / не прогрузился
К сожалению, как бы я ни хотел сделать для пользователя всё магически рабочим, ответственность за надёжность и стабильность в перечисленных случаях ложится на него. Но я могу предложить варианты и реальные практики для решения таких проблем:
- Использовать область поиска для элемента (сначала ищем стабильный элемент, а потом среди его дочерних)
- Иногда можно использовать MatchIndex/MatchReverse (поиск по индексу среди подходящих)
- Использовать скриншот-тестирование, в частности можно записать эталон самого элемента и сравнивать только его
- Использовать умное ожидание элементов, wait’ы (мы пробуем найти элемент на экране пока не наступит таймаут)
- Якориться на соседние или связанные элементы и действовать от них (в совсем крайних случаях можно программно перемещать курсор или кликать по координатам)
- Не забывать про хоткеи в программах и взаимодействовать через них (в моей реализации есть для этого SendKeys)
- В веб режиме можно выполнить JS скрипт прямо в странице, для win-программ обращаться через API если вдруг есть такая возможность
Чтобы удобно применять всё вышеперечисленное я написал библиотеку для взаимодействия с UI, которая максимально абстрагирует нас от прямой работы с данными об элементах и сводит алгоритм к примерно такому коду:
Скрытый текст
service.Wait(element_attributes); var webElement = service.Find<WebElement>(element_attributes); webElement.Click();
Описание того, как устроено взаимодействие на более низком уровне я бы хотел сделать в отдельной статье, там множество своих нюансов и заодно хочется показать как можно управлять интерфейсом через Win32 API. Работа с браузерами (Web режим) Изначально я думал что получится не заморачиваться и использовать фреймворк вроде Selenium, потому что в нём фактически реализован подход который я описал чуть выше: код прячет от программиста внутреннюю работу с интерфейсом и заморочки с атрибутами и поиском. При первом же столкновении с реальностью стало понятно что мне не хватает возможностей и всё сводилось к вызову скриптов, не было возможности (по крайней мере в тот момент) работать с окнами, вкладками и прочее. Я перешёл к более низкоуровневому подходу: написал своё браузерное расширение с использованием native messaging - это считается целевым решением когда необходимо обмениваться сообщениями между десктопным приложением и расширением и про это у меня есть отдельная статья с гайдом по настройке.

Примерное количество атрибутов у веб элемента Но и это мне не подошло, так что я перешёл к веб сокетам, потому что сама идея native messaging мне не подходила и дестабилизировала работу. Основные причины: общение через консоль и таскание в дистрибутиве лишнего exe, для которого требовалась лишняя настройка окружения. Так что я не вижу ничего плохого в том чтобы обмениваться данными через WS на localhost, тем более мы получаем возможность легко придерживаться своих понятных DTO и имеем максимальный доступ ко всему что происходит с браузером. В целом логика по получению элемента превращается в следующую цепочку действий: отдаём пользователю список запущенных приложений -> пользователь выбирает нужное, чтобы сузить контекст -> пользователь нажимает "захватить элемент" -> выбирает мышью элемент -> по координатам получаем сущность с полным описанием элемента -> парсим её и отдаём в интерфейс. В обратную сторону: пользователь выбирает прочитать текст из элемента "input" -> берём наш сохранённый описатель -> ищем элемент по атрибутам из описателя -> производим действие, возвращаем результат. Нюансы поиска элементов В момент захвата элемент существует в конкретном процессе, окне, вкладке или DOM-дереве. Но через минуту приложение может быть перезапущено, страница перезагружена, окно сдвинуто, а сам объект в памяти уже будет другим. Поэтому нам нужен не "указатель на кнопку", а набор признаков, по которым эту кнопку можно заново найти. В коде такой набор признаков превращается в дескриптор элемента, в упрощённом виде это:
Скрытый текст
public class ElementDescriptor { public string Name { get; set; } public CaptureType CaptureType { get; set; } public string? ElementType { get; set; } public List<Property> Properties { get; set; } public SearchScopeDescriptor? SearchScope { get; set; } }
Одни свойства полезны только для информации, другие слишком переменчивы, а есть те, которые хорошо подходят именно для поиска. Например, в win-режиме можно получить AutomationId, Name, ClassName, ControlType, положение элемента, состояние доступности, данные родительского окна и процесса. В идеальном мире для поиска хватило бы AutomationId, потому что это стабильный технический идентификатор. В реальности он часто пустой, повторяется или отсутствует в старых и кастомных интерфейсах. Тогда приходится комбинировать несколько признаков: тип контрола, имя, родительское окно, иногда соседние или родительские элементы. В Web-режиме история похожая, только у нас DOM и при захвате элемента можно собрать его id, name, class, tag, type, role, текст, набор aria-* атрибутов, XPath, CSS-подобные признаки, положение в дереве и другие свойства. Но здесь тоже нет одного идеального ответа. id может генерироваться фреймворком, классы могут быть хэшами, текст может зависеть от локали или тестовых данных, а XPath может развалиться после небольшого изменения верстки. Поэтому я бы разделил атрибуты на несколько групп:
- стабильные технические признаки: AutomationId, id,role, ControlType, tag
- человеческие признаки: текст кнопки, подпись поля, Name
- контекстные признаки: окно, процесс, вкладка, родительский контейнер, форма, таблица
- структурные признаки: XPath, позиция среди похожих элементов, индекс совпадения
- служебные признаки: координаты, размеры, состояние видимости, enabled/disabled
Лучше всего работают первые три группы. Структурные признаки полезны когда других вариантов нет, а координаты и размеры я бы вообще не делал основой поиска: они пригодятся только для fallback-сценариев. Чтобы пользователь не оставался с этими проблемами один на один, я добавил инструменты, которые помогают сделать поиск устойчивее:
- Внутри сам процесс поиска получается двухфазным: сначала берём все свойства, которые пользователь отметил как значимые и пытаемся получить список кандидатов, а затем, если кандидатов несколько, применяем дополнительные фильтры: регулярные выражения, wildcard-сравнение, ограничения по родителю, область поиска, индекс совпадения.
- Когда остаётся несколько похожих элементов, можно использовать MatchIndex и MatchReverse: выбрать первый, второй или последний элемент среди найденных. Это полезный механизм, но я считаю его компромиссом, ведь если порядок строк в таблице зависит от сортировки или данных, то индекс легко станет причиной падений.
- Рекомендую активно пользоваться подсветкой найденных элементов: после настройки атрибутов можно проверить, что программа считает подходящим элементом. Если подсветилось пять похожих кнопок - локатор слишком широкий, а если не подсветилось ничего - мы выбрали слишком строгие признаки или элемент ещё не появился.
- Иногда нужно ограничивать область поиска: сначала заякориться на модальном окне, таблице или форме, а уже внутри искать кнопку, поле или строку. В интерфейсе это делается в два клика и одно перетаскивание.

Наглядно - как влияют на результат выбранные параметры Нюансы управления элементами После того как элемент найден всё тоже не так просто: "кликнуть" можно разными способами, а ввод текста через клавиатуру и установка значения напрямую - это не одно и то же. Нажатие кнопки, ввод текста, выбор значения из списка или чтение состояния могут выполняться разными способами в зависимости от того, Web это или Windows, какой тип контрола перед нами и какие функции он поддерживает. Я старался привести это в библиотеке к единой модели: у элемента есть тип, а у типа есть набор доступных действий. Например, кнопка умеет нажиматься, текстовое поле умеет принимать текст и отдавать значение, чекбокс умеет переключаться, таблица - отдавать ячейки или строки, окно - активироваться, закрываться или менять размер. Такая абстракция позволяет, чтобы на уровне сценария шаг выглядел примерно так:
Скрытый текст
public class Step { public ElementDescriptor Element { get; set; } public string TypeName { get; set; } public string ActionName { get; set; } public List<MethodParameter> Parameters { get; set; } public int AutoWaitMs { get; set; } public AutoWaitOptions AutoWaitOptions { get; set; } public string ResultVariable { get; set; } }
В win-режиме часть действий можно выполнять через UIAutomation patterns: например, InvokePattern для кнопок, ValuePattern для полей ввода, SelectionItemPattern для элементов списков. Если кнопка переехала на 20 пикселей, логическое нажатие всё равно должно сработать, но UIA паттерны доступны не всегда. Некоторые контролы не реализуют нужный паттерн, некоторые отдают странные состояния, а некоторые старые интерфейсы вообще не работают с UIAutomation. Поэтому приходится иметь запасные варианты: Win32-вызовы, SendKeys, иногда координатные действия, но я стараюсь относиться к этому как к fallback, а не основному способу управления. В Web-режиме похожая логика. Если нужно нажать кнопку, можно вызвать DOM-событие или использовать браузерный API. Если нужно записать значение в input, мало просто поменять value: современные фронтенд-фреймворки часто ждут цепочку событий input, change, иногда blur. Поэтому действие должно не только изменить свойство элемента, но и сделать это так, чтобы страница восприняла изменение как пользовательский ввод. Перед выполнением команды полезно убедиться, что элемент действительно готов к действию. Для этого я разделяю обычное ожидание и готовность элемента. У меня в программе реализован настраиваемый Auto-wait перед шагом, который смотрит что: элемент найден, видим, доступен, не перекрыт другим элементом, перестал двигаться, прокручен в область видимости. Это защита от ситуации, когда сценарий пытается нажать слишком рано. В Web-режиме особенно важны проверки "visible", "enabled", "uncovered" и "stable". В Windows-приложениях картина похожая: окно может уже существовать, но контрол ещё не готов принимать действие. Ещё важно, что управление элементом должно возвращать результат. Если мы прочитали текст, хорошо сохранить его в переменную и использовать дальше. Если вызвали действие, полезно проверить результат этого действия. Поэтому после обычного шага можно добавить проверку возвращаемого значения: равно, не равно, содержит, соответствует регулярному выражению и так далее. Это делает сценарий не просто последовательностью команд, а проверяемой цепочкой. Такой подход не убирает всю сложность UI-автоматизации, но пользователь видит понятный шаг "нажать кнопку" или "ввести текст", а внутри библиотека выбирает, как именно это сделать для Windows или браузера. И если что-то падает, видно причину: элемент не найден/не тот, команда не поддерживается или результат не совпал с ожидаемым. И чтобы ещё сильнее компенсировать сложности я добавил режим отладки, чтобы пошагово следить за выполнением.

Пример создания элементарного алгоритма Как сценарий стал тестом На этом этапе у нас уже есть всё, чтобы управлять интерфейсом: мы умеем находить элементы, ждать их готовности и выполнять над ними действия. Но сам по себе набор действий ещё не делает сценарий тестом. Нам нужен функционал, отвечающий за тестовую логику: проверки, данные, отчёты, отладку, логи и анализ падений. Поэтому дальше начинают появляться специальные шаги сценария, которые отвечают уже не за управление интерфейсом, а за индикацию того что всё выглядит как задумано и за аналитику работы сценария. Самый простой пример - авторизация. Если сценарий ввёл логин, пароль и нажал "Войти", это ещё не тест. Возможно, кнопка не сработала, форма показала ошибку, пользователь остался на той же странице, но сам клик технически прошёл успешно. Тест обычно выгдядит так: после входа должен появиться такой-то элемент, текст должен быть таким-то, API должен вернуть статус 200, а блок профиля должен визуально соответствовать эталону. Wait: ждать не время, а состояние Первое, что нужно почти любому UI-сценарию, - нормальные ожидания. Не sleep(3000), а ожидание конкретного состояния. Поэтому отдельный шаг Wait я воспринимаю не как паузу, а как часть смысла сценария. Он отвечает на вопрос: "какое состояние приложения должно наступить, прежде чем мы пойдём дальше?". Например:
- элемент появился
- элемент исчез
- текст изменился
- окно стало доступно
- значение стало равно ожидаемому
В ручном тест-кейсе мы тоже пишем не "подождать 2 секунды", а "дождаться появления сообщения об успешном сохранении".

Ожидание загрузки после запуска Assert: проверять и сравнивать результат Проверка должна стоять рядом с действием, которое породило результат. Если мы нажали "Сохранить", лучше сразу дождаться сообщения и проверить его текст. Если проверку оставить только в самом конце, сценарий может долго идти по неправильному состоянию, а причина падения станет менее очевидной. Также полезны разные типы сравнения: точное равенство, "содержит", регулярное выражение, больше/меньше и прочее. На практике UI часто возвращает текст с пробелами, префиксами, динамическими частями. Поэтому строгая проверка нужна не всегда, иногда надёжнее проверять именно значимую часть результата.

Пример того, как можно валидировать результат: в самом действии или отдельным шагом По поводу переменных: если сценарий начинает жить дольше одного запуска, в нём появляются логины, номера заявок, пути к файлам, ожидаемые статусы. Должна быть возможность переиспользовать и загружать такие данные, для этого я добавил переменные и их загрузку из файлов. С LoadDataможно вынести пары "имя переменной - значение" в CSV, XLS или XLSX и подставлять их в шаги, это уже достаточно сильно снижает дублирование. Визуальные и API проверки Есть случаи, когда результат невозможно валидировать через обычные проверки атрибутов и текста. Например, съехала верстка, кнопка стала неактивной визуально или поменялся цвет. Для таких случаев я добавил скриншот-тестирование. Наиболее практичный подход - сравнивать минимальную полезную область или, по возможности, только один элемент. В идеале перед сравнением нужно стабилизировать картинку: дождаться окончания загрузки, отключить анимации, скрыть динамические блоки, если они не относятся к проверке. Тогда визуальная проверка становится нормальным инструментом для тестирования интерфейса. Можно сравнивать скриншот всего экрана, но он почти всегда шумный: время, анимации, случайные данные, разные размеры окна, системные элементы.

Пример успешного и неуспешного сценариев тестирования Также иногда есть необходимость отправить HTTP-запрос, чтобы проверить статус и другие значения из ответа. Для этого я добавил возможность работы с API, оставив в быстром доступе проверки возвращаемых значений и снятие временных метрик. Примерный сценарий когда это может пригодиться выглядит так:
- Создать объект через интерфейс
- Прочитать его номер из сообщения на экране
- Вызвать API по этому номеру
- Проверить статус и поля в JSON
- Вернуться в UI и убедиться, что объект отображается в списке

Пример проверки API-запроса При этом я не хотел превращать визуальный сценарий в скрытый код, который понятен только автору. Хороший сценарий должен читаться как расширенный ручной тест-кейс: нажать, дождаться, проверить, сохранить значение, вызвать API, сравнить результат. Если тестировщик открывает алгоритм через месяц, он должен понять не только что делает сценарий, но и зачем каждый шаг там стоит. Поэтому после действий и проверок неизбежно приходим к отладке, журналам и отчётам. Отчётность и результаты тестов Когда сценарий падает, нам мало знать "не прошёл шаг 17", ведь нужно понять: что это был за шаг, какой элемент искался, сколько времени он ждал, какой результат вернул и в чём конкретно заключалась ошибка. Поэтому вокруг выполнения сценария появились отладка, журнал шагов, история запусков и скриншот при падении. Для регулярных запусков важны отчёты не только в интерфейсе программы, но и в формате, который можно передать дальше: JSON, JUnit, Allure. Тогда визуально собранный сценарий становится ближе к обычному тестовому прогону: его можно запускать, анализировать, хранить историю и подключать к процессу команды.

Пример создания набора сценариев И здесь, кажется, можно остановиться и вернуться к исходной идее всей утилиты. Я не пытался заменить кодовую автоматизацию там, где она уже хорошо выстроена. Мне скорее хотелось закрыть промежуток между ручной регрессией и полноценным фреймворком: дать возможность быстро собрать понятный сценарий, постепенно укрепить его проверками и затем запускать его как нормальный тестовый прогон. Но тем не менее граница применимости инструмента остаётся на усмотрение пользователя: в каких задачах визуальная сборка действительно экономит время, а где проще писать автотесты кодом. Куда дальше Интерфейсы разные, команды разные, привычки и навыки у тестировщиков тоже разные. Поэтому развитие я вижу в нескольких важных направлениях:
- Более умная работа с локаторами. Сейчас инструмент подсказывает какие атрибуты нестабильны и много берёт на себя, но хочется совсем освободить пользователя от нюансов работы с UI и скрыть эти настройки. В идеале сделать целевой кнопку "Record", которая просто запишет все ручные манипуляции с интерфейсом за пользователем.
- Улучшение анализа падений. Я определённо хочу доработать визуализацию истории запусков и сделать для пользователя каждый прогон предельно информативным и понятным. Стремлюсь к тому чтобы шаг сценария содержал максимум данных о том что он делает или сделал.
- Больше платформ и типов проверок. Сейчас основной фокус у меня на Windows и Web, но подход с дескрипторами, ожиданиями и прочим можно переносить дальше. Очень хочу и планирую совместить программу с тестированием мобильных устройств.
- Интеграция с привычными процессами. Пользователь не должен быть заперт внутри программы, я уже постарался дать экспорт в код, который может быть интересен для разработчиков. Плюс отчёты в JUnit, Allure и JSON, которые важны как мост к CI и командной разработке. Сейчас я активно дорабатываю это направление, но требуется больше обратной связи.
Заключение Инструмент полезен для сложных\старых интерфейсов, нетипичных проверок и сложных сценариев в которых может быть одновременно работа и с виндоус приложением, и с вебом. В таких условиях он должен стать удобнее чем, например, Playwright, в котором даже с использованием кодекса получится длинный скрипт, который нужно поддерживать вместе со всеми локаторами. Это сложно без навыков программирования, а я делаю утилиту где всё сразу под рукой и можно смотреть на алгоритм через визуальный конструктор и отлаживать его. Если вы захотите попробовать RTHelper (нет обязательной регистрации), то можно начать с самого простого: открыть форму, заполнить поля, нажать кнопку, проверить результат. Если его получится довести хотя бы до нескольких стабильных запусков подряд, сразу станет понятно, где инструмент экономит время, а где чего-то не хватает. Я буду рад обратной связи, особенно критичной, чтобы инструмент был действительно рабочий. Интересно где всё сработало и где сломалось: какие интерфейсы не работают, каких действий не хватило, где отчёт оказался непонятным или какие сценарии всё равно проще писать кодом. Для развития было бы здорово не угадывать абстрактные потребности, а смотреть на реальные проблемы в применении.-Источник
|