Привет, Хабр! Меня зовут Вера Соколова, я Android-разработчик в команде автотестирования проекта Мой МТС.
Автотесты — очень полезная штука в разработке, так как они упрощают жизнь ручным тестировщикам. На релизе во время smoke-прогона у коллег и так очень много работы, а тут небольшая, но порой ощутимая помощь в виде автотестов, когда после запуска руками тестировщики проверяют только то, что не автоматизировано или если тесты где-то не прошли.
Но автотесты нужно еще написать и первостепенная задача — поиск элементов. Чтобы кликнуть на кнопочку, ее нужно сначала найти. С какими трудностями при этом можно столкнуться и как эти трудности преодолеть я расскажу ниже, добро пожаловать под кат!
А зачем вообще искать?
Любой тест — это взаимодействие. В случае с автотестированием — это взаимодействие с элементами на экране мобильного телефона: кнопками, текстами, чекбоксами, диалогами, таббарами. Когда ручной тестировщик прокликивает элементы — у него не возникает вопросов, куда конкретно кликать, это описано в кейсе. Но как научить машину кликать на «вон ту большую красную кнопку»? Задача усложняется, если таких больших красных кнопок на экране несколько, а система блочная, элементы в ней друг от друга не зависят и переиспользуются.
Что мы подразумеваем под уникальным элементом? Это элемент, который можно идентифицировать на экране в единственном экземпляре. Всего одна большая красная кнопка, всего один заголовок таббара, всего одна кнопка «назад». Яркий пример не уникальных элементов — повторяющиеся айтемы списка, с ними часто возникают проблемы. У каждого элемента должно быть что-то особенное, отличительная черта, иначе драйвер его не найдет.
Казалось бы, нужно взять и найти такие элементы, в чем проблема-то? Но не все так просто, как кажется.
Стратегии поиска элементов
В зависимости от используемого драйвера/платформы есть несколько вариантов поиска. В нашем случае речь пойдет про Android и 2 драйвера — UIAutomator2 и Espresso. В таблице — стратегии, которые мы использовали в автотестах:
Рассмотрим стратегии по отдельности.
Content description
Content description — в дословном переводе это «описание контента». (cпасибо, кэп!) Этот параметр в android задуман как метка, позволяющая пользователям с ограниченными возможностями распознавать элементы интерфейса при помощи программы чтения с экрана. А теперь представьте какой-нибудь аватар на главном экране приложения. Если в атрибут content-desk этого элемента зашить значение вида avatar_main_screen, то при чтении программой пользователь явно услышит не то, что должен. Поэтому от Content description мы отказались почти сразу.
ID и первые проблемы. Нумерация элементов и необходимость наличия в ресурсах
Использование идентификаторов — это самый предпочтительный способ. Но есть нюансы. Казалось бы, пронумеруй все элементы списка, проставь динамически идентификаторы во время выполнения программы в зависимости от контента и готово. Не все так просто.
Допустим, у нас есть некий список, который состоит из айтемов:
Проблема 1: при динамической смене id конечный идентификатор должен быть прописан в ресурсах приложения. Если список состоит из 10 элементов, в ресурсах должны быть прописаны все 10 id: item0, item1, … item9, иначе во время выполнения при попытке сменить идентификатор приложение упадет, не найдя нужный id.
Решение: в Android-проекте нужно добавить обработку ошибки Resource not found exception. Так мы изменяем только те идентификаторы элементов, которые прописаны в ресурсах.
Проблема 2: Каждый айтем — это контейнер с некоторым набором элементов (иконка, текст, описание). Представьте, что для каждого такого элемента придётся в ресурсах прописывать id. Тогда они быстро превратятся в помойку из кучи пронумерованных элементов.
Решение: нужно нумеровать только контейнеры (родительские блоки) первых 10-15 элементов. Для тестов этого количества обычно достаточно. Элементы внутри контейнера чаще всего уникальные. Далее при написании теста сначала ищем родителя, а по нему уже находим нужный элемент.
XPath
XPath — поиск элементов по дереву xml. Это, наверное, самая нежелательная стратегия поиска. Но она самая доступная, поскольку все остальные варианты требуют дополнительных манипуляций со стороны приложения.
Во-первых, здесь возможны проблемы с производительностью. Во-вторых, конструкция xpath неустойчива. Стоит в разметке приложения добавить дополнительную вложенность и тесты улетят в трубу. И в-третьих, ощутимо усложняется читаемость в коде автотестов. Представьте, что каждое объявление элементов выглядит вот так:
Мы используем XPath для ускорения написания теста, когда идентификатора нет и разработчику нужно время на то, чтобы добавить его. Выручает он и в тех случаях, когда нужно обратиться к каким-либо системным элементам, например, к ланчеру Android или залезть в настройки, тогда без этого инструмента никак. А также в тех случаях, когда остальные стратегии поиска недоступны.
Работу можно упростить, используя возможности и сокращения языка xpath. Для упрощения составления более короткого xpath удобно использовать онлайн-инструменты, например раз и два. Есть множество сервисов, в которых можно подобрать более короткую комбинацию для xpath, предварительно загнав xml разметки экрана.
Мы пришли также к составлению xpath для поиска более чем по одному родителю, когда в некоторую функцию передаем список идентификаторов, а на выходе получаем склеенную строчку xpath:
Идентификаторы последовательно передаются через запятую в порядке от родителя верхнего уровня до конечного элемента. Уровень вложенности значения не имеет. Главное — уникальность каждого n-ого родителя среди прочих контейнеров.
Теги и эспрессо
Эспрессо — это мощный инструмент, но для его настройки нужны время и силы. Помимо того, что эспрессо должен быть зашит непосредственно в основной проект и иметь определенные зависимости, есть еще несколько маленьких, но очень неприятных подводных камней, с которыми мы столкнулись:
1) Файлик build_config.xml с описанием параметров зашивается в проект автотестов и содержит информацию о min sdk, версию грэдла и другие данные. Они нужны как ключ к основному проекту. То есть эти данные повторяют данные, зашитые в основном проекте.
Местоположение файла нужно указать в капабилити с ключом appium:espressoBuildConfig.
2) Конфликт ресурсов. Когда смежные библиотеки имеют одни и те же идентификаторы ресурсов — тесты не стартанут. Решение: прописать в файле build_config.xml исключения:
3) Периодические ошибки вроде “драйвер не стартанул”, особенно при первом его запуске. Тут пока сделать ничего не удалось, приходится страдать.
4) На момент написания статьи, для тестов с Espresso-driver в проекте используется последняя стабильная версия appium 1.22.0. Android-проект перешёл на использование agp 7 (android gradle plugin) и java 11. И возникла проблема со сборкой appium-espresso-driver в связи с несовместимыми изменениями, которые поломали билд конфиг драйвера. В репозитории appium-espresso-driver уже висит открытый ПР на эту тему, но пока мы ждем когда его смержат.
Несмотря на все минусы тегов в их использовании есть существенный плюс по сравнению с id: теги можно менять во время работы приложения и ни в какие ресурсы прописывать не надо. Однако подружиться с Espresso окончательно так и не удалось, поэтому теги используются крайне редко.
Про внешние сдк, внедрённые в проект
С сдк вариантов два: либо использовать локаторы в том виде, в котором они уже зашиты разработчиками сторонних библиотек, либо расковыривать библиотеку и переписывать как надо. Нужно ли это вообще? Скорее всего нет.
А есть ли разница, телефон или планшет?
В целом разница есть, хоть и небольшая, так как при оптимизации под планшеты может использоваться другая разметка экранов.
А как это помогает?
Автотесты дают нам как минимум — более читаемый код, как максимум — счастливых ручных тестировщиков, которым на релизе нужно проверять значительно меньше кейсов. Например, в последнем релизном прогоне было 183 кейса. 22 упало, 22 скрыто, итого — 140 кейсов на сегодняшний день.
Надеюсь, что наш опыт в поиске уникальных элементов будет вам полезен. С радостью отвечу на вопросы в комментариях! Если же у вас есть свои лайфхаки при поиске той самой большой красной кнопки на экране при написании автотестов — обязательно расскажите о нем.
Автотесты — очень полезная штука в разработке, так как они упрощают жизнь ручным тестировщикам. На релизе во время smoke-прогона у коллег и так очень много работы, а тут небольшая, но порой ощутимая помощь в виде автотестов, когда после запуска руками тестировщики проверяют только то, что не автоматизировано или если тесты где-то не прошли.
Но автотесты нужно еще написать и первостепенная задача — поиск элементов. Чтобы кликнуть на кнопочку, ее нужно сначала найти. С какими трудностями при этом можно столкнуться и как эти трудности преодолеть я расскажу ниже, добро пожаловать под кат!
А зачем вообще искать?
Любой тест — это взаимодействие. В случае с автотестированием — это взаимодействие с элементами на экране мобильного телефона: кнопками, текстами, чекбоксами, диалогами, таббарами. Когда ручной тестировщик прокликивает элементы — у него не возникает вопросов, куда конкретно кликать, это описано в кейсе. Но как научить машину кликать на «вон ту большую красную кнопку»? Задача усложняется, если таких больших красных кнопок на экране несколько, а система блочная, элементы в ней друг от друга не зависят и переиспользуются.
Что мы подразумеваем под уникальным элементом? Это элемент, который можно идентифицировать на экране в единственном экземпляре. Всего одна большая красная кнопка, всего один заголовок таббара, всего одна кнопка «назад». Яркий пример не уникальных элементов — повторяющиеся айтемы списка, с ними часто возникают проблемы. У каждого элемента должно быть что-то особенное, отличительная черта, иначе драйвер его не найдет.
Казалось бы, нужно взять и найти такие элементы, в чем проблема-то? Но не все так просто, как кажется.
Стратегии поиска элементов
В зависимости от используемого драйвера/платформы есть несколько вариантов поиска. В нашем случае речь пойдет про Android и 2 драйвера — UIAutomator2 и Espresso. В таблице — стратегии, которые мы использовали в автотестах:
Рассмотрим стратегии по отдельности.
Content description
Content description — в дословном переводе это «описание контента». (cпасибо, кэп!) Этот параметр в android задуман как метка, позволяющая пользователям с ограниченными возможностями распознавать элементы интерфейса при помощи программы чтения с экрана. А теперь представьте какой-нибудь аватар на главном экране приложения. Если в атрибут content-desk этого элемента зашить значение вида avatar_main_screen, то при чтении программой пользователь явно услышит не то, что должен. Поэтому от Content description мы отказались почти сразу.
ID и первые проблемы. Нумерация элементов и необходимость наличия в ресурсах
Использование идентификаторов — это самый предпочтительный способ. Но есть нюансы. Казалось бы, пронумеруй все элементы списка, проставь динамически идентификаторы во время выполнения программы в зависимости от контента и готово. Не все так просто.
Допустим, у нас есть некий список, который состоит из айтемов:
Проблема 1: при динамической смене id конечный идентификатор должен быть прописан в ресурсах приложения. Если список состоит из 10 элементов, в ресурсах должны быть прописаны все 10 id: item0, item1, … item9, иначе во время выполнения при попытке сменить идентификатор приложение упадет, не найдя нужный id.
<item type="id" name="item"/>
<item type="id" name="item0"/>
<item type="id" name="item1"/>
<item type="id" name="item2"/>
<item type="id" name="item3"/>
<item type="id" name="item4"/>
<item type="id" name="item5"/>
<item type="id" name="item6"/>
<item type="id" name="item7"/>
<item type="id" name="item8"/>
<item type="id" name="item9"/>
Решение: в Android-проекте нужно добавить обработку ошибки Resource not found exception. Так мы изменяем только те идентификаторы элементов, которые прописаны в ресурсах.
Проблема 2: Каждый айтем — это контейнер с некоторым набором элементов (иконка, текст, описание). Представьте, что для каждого такого элемента придётся в ресурсах прописывать id. Тогда они быстро превратятся в помойку из кучи пронумерованных элементов.
<!--Блок item_container-->
<item type="id" name="itemContainer"/>
<item type="id" name="itemContainer0"/>
<item type="id" name="itemContainer1"/>
<item type="id" name="itemContainer2"/>
<item type="id" name="itemContainer3"/>
<item type="id" name="itemContainer4"/>
<item type="id" name="itemContainer5"/>
<item type="id" name="itemContainer6"/>
<item type="id" name="itemContainer7"/>
<item type="id" name="itemContainer8"/>
<item type="id" name="itemContainer9"/>
<item type="id" name="itemTitle"/>
<item type="id" name="itemTitle0"/>
<item type="id" name="itemTitle1"/>
<item type="id" name="itemTitle2"/>
<item type="id" name="itemTitle3"/>
<item type="id" name="itemTitle4"/>
<item type="id" name="itemTitle5"/>
<item type="id" name="itemTitle6"/>
<item type="id" name="itemTitle7"/>
<item type="id" name="itemTitle8"/>
<item type="id" name="itemTitle9"/>
<item type="id" name="itemIcon"/>
<item type="id" name="itemIcon0"/>
<item type="id" name="itemIcon1"/>
<item type="id" name="itemIcon2"/>
<item type="id" name="itemIcon3"/>
<item type="id" name="itemIcon4"/>
<item type="id" name="itemIcon5"/>
<item type="id" name="itemIcon6"/>
<item type="id" name="itemIcon7"/>
<item type="id" name="itemIcon8"/>
<item type="id" name="itemIcon9"/>
Решение: нужно нумеровать только контейнеры (родительские блоки) первых 10-15 элементов. Для тестов этого количества обычно достаточно. Элементы внутри контейнера чаще всего уникальные. Далее при написании теста сначала ищем родителя, а по нему уже находим нужный элемент.
XPath
XPath — поиск элементов по дереву xml. Это, наверное, самая нежелательная стратегия поиска. Но она самая доступная, поскольку все остальные варианты требуют дополнительных манипуляций со стороны приложения.
Во-первых, здесь возможны проблемы с производительностью. Во-вторых, конструкция xpath неустойчива. Стоит в разметке приложения добавить дополнительную вложенность и тесты улетят в трубу. И в-третьих, ощутимо усложняется читаемость в коде автотестов. Представьте, что каждое объявление элементов выглядит вот так:
public final By redButton = By.xpath("/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout");
Мы используем XPath для ускорения написания теста, когда идентификатора нет и разработчику нужно время на то, чтобы добавить его. Выручает он и в тех случаях, когда нужно обратиться к каким-либо системным элементам, например, к ланчеру Android или залезть в настройки, тогда без этого инструмента никак. А также в тех случаях, когда остальные стратегии поиска недоступны.
Работу можно упростить, используя возможности и сокращения языка xpath. Для упрощения составления более короткого xpath удобно использовать онлайн-инструменты, например раз и два. Есть множество сервисов, в которых можно подобрать более короткую комбинацию для xpath, предварительно загнав xml разметки экрана.
Мы пришли также к составлению xpath для поиска более чем по одному родителю, когда в некоторую функцию передаем список идентификаторов, а на выходе получаем склеенную строчку xpath:
public By androidXpath(String... elementIds) {
StringBuilder xpath = new StringBuilder();
for (String elementId : elementIds) {
xpath.append("//*[ends-with(@resource-id,\"/").append(elementId).append("\")]");
}
return By.xpath(xpath.toString());
}
Идентификаторы последовательно передаются через запятую в порядке от родителя верхнего уровня до конечного элемента. Уровень вложенности значения не имеет. Главное — уникальность каждого n-ого родителя среди прочих контейнеров.
Теги и эспрессо
Эспрессо — это мощный инструмент, но для его настройки нужны время и силы. Помимо того, что эспрессо должен быть зашит непосредственно в основной проект и иметь определенные зависимости, есть еще несколько маленьких, но очень неприятных подводных камней, с которыми мы столкнулись:
1) Файлик build_config.xml с описанием параметров зашивается в проект автотестов и содержит информацию о min sdk, версию грэдла и другие данные. Они нужны как ключ к основному проекту. То есть эти данные повторяют данные, зашитые в основном проекте.
{
"toolsVersions": {
"gradle": "5.6",
"androidGradlePlugin": "3.4.2",
"compileSdk": 28,
"buildTools": "28.0.3",
"minSdk": 18,
"targetSdk": 28,
"kotlin": "1.3.71"
}
}
Местоположение файла нужно указать в капабилити с ключом appium:espressoBuildConfig.
2) Конфликт ресурсов. Когда смежные библиотеки имеют одни и те же идентификаторы ресурсов — тесты не стартанут. Решение: прописать в файле build_config.xml исключения:
{
"toolsVersions": {
"gradle": "5.6",
"androidGradlePlugin": "3.4.2",
"compileSdk": 28,
"buildTools": "28.0.3",
"minSdk": 18,
"targetSdk": 28,
"kotlin": "1.3.71"
},
"additionalAppDependencies": [
"com.google.android.material:material:1.0.0",
"androidx.lifecycle:lifecycle-extensions:2.1.0"
]
}
3) Периодические ошибки вроде “драйвер не стартанул”, особенно при первом его запуске. Тут пока сделать ничего не удалось, приходится страдать.
4) На момент написания статьи, для тестов с Espresso-driver в проекте используется последняя стабильная версия appium 1.22.0. Android-проект перешёл на использование agp 7 (android gradle plugin) и java 11. И возникла проблема со сборкой appium-espresso-driver в связи с несовместимыми изменениями, которые поломали билд конфиг драйвера. В репозитории appium-espresso-driver уже висит открытый ПР на эту тему, но пока мы ждем когда его смержат.
Несмотря на все минусы тегов в их использовании есть существенный плюс по сравнению с id: теги можно менять во время работы приложения и ни в какие ресурсы прописывать не надо. Однако подружиться с Espresso окончательно так и не удалось, поэтому теги используются крайне редко.
Послесловие и итоги
Про внешние сдк, внедрённые в проект
С сдк вариантов два: либо использовать локаторы в том виде, в котором они уже зашиты разработчиками сторонних библиотек, либо расковыривать библиотеку и переписывать как надо. Нужно ли это вообще? Скорее всего нет.
А есть ли разница, телефон или планшет?
В целом разница есть, хоть и небольшая, так как при оптимизации под планшеты может использоваться другая разметка экранов.
А как это помогает?
Автотесты дают нам как минимум — более читаемый код, как максимум — счастливых ручных тестировщиков, которым на релизе нужно проверять значительно меньше кейсов. Например, в последнем релизном прогоне было 183 кейса. 22 упало, 22 скрыто, итого — 140 кейсов на сегодняшний день.
Надеюсь, что наш опыт в поиске уникальных элементов будет вам полезен. С радостью отвечу на вопросы в комментариях! Если же у вас есть свои лайфхаки при поиске той самой большой красной кнопки на экране при написании автотестов — обязательно расскажите о нем.
Комментарии (4)
AliceCarroll
20.12.2021 21:36Для content description Вы можете просто написать простенькую оберточку вида
@BindingAdapter("testLabel") fun bindTestLabel(view: View, testLabel: CharSequence?) { if(BuildConfig.DEBUG && testLabel != null) { view.importantForAccessibility = 1 view.contentDescription = testLabel }}
и использовать аттрибут testLabel, который будет виден как content description только в дебажных сборках
Toliwer
Упавшие автотесты лучше, чем отсутствующие :)
vera_vera Автор
А то)))