Привет! Меня зовут Семен, я занимаюсь автоматизацией тестирования мобильных приложений в hh.ru, и сегодня я расскажу о том, как писать автотесты под iOS. 

Быстрые регрессы, низкий time-to-market, счастливые пользователи, у которых нет багов — для всего этого нам нужны автотесты. Наше мобильное приложение работает сразу на двух платформах, и мы пишем автотесты для каждой из них. 

Автотесты экономят нам сотни часов ручного тестирования, и мы всё продолжаем их писать. В этой статье я разберу процесс написания одного автотеста для IOS-приложения от и до.

С чего начать

Немного углубимся в детали. В hh.ru весь регресс полностью автоматизирован. Автоматический тест — это то же самое, что и ручное тестирование, только все действия  вместо пользователя выполняет бездушная машина: поиск элементов, взаимодействие с ними, выполнение каких-либо проверок и многое другое.

Перед тем как автоматизировать какой-либо кейс, мы должны придумать ручной тест-кейс. Для примера возьмем функционал заполнения пользователем информации о себе.

Как мы проверяем это вручную? Открываем приложение, переходим в профиль, авторизируемся пользователем с одним резюме, скролим до секции «Дополнительно», открываем ее, скролим до секции «О себе» и открываем. Печатаем какую-нибудь корректную информацию, нажимаем кнопку «Сохранить». После сохранения нас перекидывает на экран секции «Дополнительно». На этом экране проверяем, что информация сохранилась и корректно отображается.

Окей, мы придумали тест-кейс. Теперь попробуем его автоматизировать.

Но для начала стоит разобраться, какие инструменты мы используем. Есть две категории тестовых фреймворков: кроссплатформенные и нативные.

Кроссплатформенные фреймворки позволяют писать один тест сразу для двух платформ. Пример кроссплатформенного фреймворка – это Appium. Нативные фреймворки написаны под конкретную платформу и позволяют разрабатывать тесты на её языке. Для Android – это Kotlin, для IOS – это Swift.

Когда-то давно мы приняли решение использовать именно нативные фреймворки, так как считаем их более стабильными и надежными. Для Android мы используем Kaspresso, для IOS – XCUITest.

Подготовка

Перед тем, как начать автоматизировать шаги теста, нам нужно произвести некоторую подготовку. А именно – сделать Page Object экрана и проставить идентификатор элементам. 

Мы пишем тесты с использованием так называемых Page Object. Page Object’ы – это специальные классы, которые отделяют реализацию действия на экране от его использования.

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

Для нашего тестового кейса потребуются Page Object’ы нескольких экранов. Но мы рассмотрим экран попроще – «О себе». Назовем класс этого Page Object понятным именем «AboutMePageObject».

AboutMePageObject
AboutMePageObject

 А файл, в котором будут написаны тесты для данного экрана, «AboutMeTestSuit».

AboutMeTestSuit
AboutMeTestSuit

Идентификаторы

Теперь разберемся с идентификаторами. Каждый элемент на экране должен иметь уникальный идентификатор, по которому XCUITest будет однозначно его находить. В iOS мы для этого используем «Accessibility identifier» – это строка, которая присваивается вашему элементу, и по которой в дальнейшем данный элемент находится на экране.

Как правило, идентификатор – это уникальная строка. Элементов на экране может быть множество, и мы должны понимать, с каким конкретно элементом взаимодействуем. Допустим, выдача почти одинаковых вакансий.

Выдача вакансий
Выдача вакансий

Такие кейсы тоже нужно рассматривать и не забывать про них при установке идентификатора. Иначе при выполнении теста XCode выдаст ошибку, потому что не сможет понять, с каким конкретно элементом ему нужно взаимодействовать.

Теперь давайте наконец попробуем автоматизировать наш тест, о котором мы говорили ранее. Шаги нашего автоматического теста будут аналогичны шагам ручного теста. Осталось всё это дело автоматизировать.

Первым делом зададим идентификаторы элементам, которые находятся на экране, и с которыми мы будем взаимодействовать. Это поле ввода, кнопка «Сохранить» в нижней части экрана, кнопка «Очистить» в верхнем правом углу экрана и кнопка «Назад» в верхнем левом углу экрана.

Идентификаторы проставляются в коде в тех местах, где создается или обновляется наш элемент: ячейка, кнопка или что-то другое. Чтобы добавить идентификатор элементу, нужно собрать билд и открыть иерархию элементов. Это делается для нашего удобства, так как с помощью иерархии элементов мы сможем найти файл, в котором создается или обновляется наш элемент.

Иерархия элементов

Итак, собираем билд и открываем иерархию элементов. В левой части экрана находится сама иерархия элементов. Находим нужный нам элемент – инпут ввода текста. Это легко.

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

Теперь нужно понять, есть ли идентификатор у данного элемента. Возможно, он уже был установлен до нас. Для этого посмотрим в правую часть экрана и найдем строку identifier. Она пустая. Значит идентификатор не установлен.

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

Открываем нужный нам файл, находим место, где создается или обновляется элемент. Как правило, это метод setup или update. 

Итак, мы нашли необходимое место. Теперь нужно добавить идентификаторы. Как я уже говорил, они должны быть уникальными. Например, не стоит вьюшкам разных экранов делать одну и ту же метку — «view». Так как при взаимодействии с ними XCode не сможет понять, с каким конкретно элементом он взаимодействует.

Правильнее будет добавить к данному идентификатору уникальное значение. Допустим, резюме. Таким образом, view экрана резюме будет называться «ResumeView». 

Чтобы не засорять XCode одинаковыми строками и уменьшить риск опечаток, мы вынесли все строки в специальный файл «Accessibility».  

Теперь мы можем добавить идентификатор в нужном месте.

Теперь проверим, что всё работает.  Для этого пересоберем билд и запустим иерархию элементов еще раз.

На скриншоте видно, что теперь идентификатор установлен. 

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

Итак, для кнопки «Сохранить» мы установили  идентификатор «SaveUIButton».

Мы задали идентификаторы нужным нам элементам. Теперь нужно сделать Page Object этого экрана – «AboutMePageObject». В Page Object описаны не только действия, которые можно совершать на данном экране, но и элементы, которым мы присвоили идентификаторы.

Давайте создадим две переменные: «AboutMeTextInputField» и «SaveButton». И зададим, что искать эти элементы нужно по указанным идентификаторам.

Теперь вспомним наш тест-кейс. Нам нужно ввести текст и нажать на кнопку «Сохранить». Именно такой метод и сделаем — «TypeAndSaveText», который на вход будет принимать строку. Рассмотрим, что же конкретно мы делаем.

Нажимаем на инпут, вводим текст, нажимаем на кнопку «Сохранить». С каждым элементом мы взаимодействуем с помощью специальных методов, встроенных в XCUITest.

Например, TypeText  — это метод, который позволяет печатать текст. Метод «TypeTextAndSave» возвращает  нам Page Object следующего экрана, на который мы попадаем после нажатия на кнопку «Сохранить». Вот так. Мы сделали один простой метод для нашего простого тест-кейса.

Тест!

Наконец-то можно переходить к самому тесту. Шаги нашего автоматического теста будут аналогичны шагам ручного теста. Получившийся тест выглядит следующим образом.

Разберем его немного подробнее.

В самом начале вы можете увидеть некоторые, так скажем, подготовительные процедуры: инициализацию фикстур, запуск приложения и многое другое. Далее начинается сам тест.

Первым делом с помощью фикстур создаем резюме. Поле «О себе» не заполнено. Это специальное предусловие нашего теста. Далее мы инициализируем Page Object главного экрана для того, чтобы перейти в  таб-баре на профиль.

Следующий шаг – это открытие секции «Дополнительно». Этот метод включает в себя скролл до секции и нажатие на саму секцию. Следующий шаг – это открытие секции «О себе». Наконец мы приблизились к методу, который и сделали – «TypeAndSaveText», в который мы и передали строку.

После выполнения данного метода мы попадаем на предыдущий экран – на экран секции «Дополнительно». Следующий метод проверяет, что введенный текст сохранен и отображается корректно.

Браво, мы сделали автотест.

Заключение

Итак, мы разобрались, как и для чего делается Page Object, как писать тест и, самое главное, мы научились как проставлять идентификаторы элементам.

У этой статьи есть видеоверсия, жмите сюда, если хотите посмотреть на создание теста в динамике.

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

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

Комментарии (1)


  1. amedvedjev
    27.01.2022 17:13
    +1

    А у нас был похожий опыт!

    Я начал проставлять айдишки в приложении iOS сам. Позднее девелоперы увидев помогли сделать генерацию всех айдишек автоматически если билд не релиз или не продакшн.

    пример:

    <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="111" width="375" height="56" name="generated_MultipleBalanceView">
    <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="111" width="375" height="56" name="generated_UIStackView">
    <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="95" y="111" width="185" height="56" name="generated_UIStackView">
    <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="95" y="111" width="107" height="56" name="generated_AnimatingBalanceLabel">
    <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" enabled="true" visible="false" x="95" y="111" width="107" height="56" name="balanceLabel" label="£0,00" value="£0,00">

    таким образом автоматом были доступны мне (я на аппиуме пишу) хоть какие-то ид чтобы писать не зализая в код iOS.

    сейчас я уже не часто меняю сгенеренную идшку на свою (тут в примере - `balanceLabel`).

    метод уже прижился в нескольких фирмах. реализация правда сделана чуть по разному.

    но в обоих меняю ид (если надо) крайне удобно добавив '.id("myID")' к любому элементу:

    let saveAmountLabel = Label().id("saveAmountLabel").style(.amount)

    вот такой опыт.

    Кстати по поводу одиноковых элементов в Аппиуме прекрасные варианты как искать типа:

        // chain: generated_UINavigationBar -> generated_UINavigationBar
        @iOSXCUITFindBys(value = {
                @iOSXCUITBy(id = "generated_UINavigationBar"),
                @iOSXCUITBy(id = "generated__UIButtonBarButton")
        })
        
        // all: X1View or X2View
        @iOSXCUITFindAll(value = {
                @iOSXCUITBy(id = "X1View"),
                @iOSXCUITBy(id = "X2View")
        })
        
        // all chain all: any X1View or X2View -> any X3View or X4View
        @iOSXCUITFindAll(value = {
                @iOSXCUITBy(id = "X1View")
                @iOSXCUITBy(id = "X2View")
        })
        @iOSXCUITFindAll(value = {
                @iOSXCUITBy(id = "X3View"),
                @iOSXCUITBy(id = "X4View")
        }, priority = 1)
        
        // classChain: get second 'generated_LinkButton' then inside first 'XCUIElementTypeStaticText'
        @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`name == \"generated_LinkButton\"`][2]/**/XCUIElementTypeStaticText")
        
        // predicate class 'XCUIElementTypeImage' with name contains 'cardActiveImage_'
        @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeImage' AND name CONTAINS[cd] 'cardActiveImage_'")
        
        // now any combination of all above :-)