Меня зовут Дмитрий Макаренко, я Mobile QA Engineer в Badoo и Bumble: занимаюсь тестированием новой функциональности в наших приложениях вручную и покрытием её автотестами.
За последние два года подход к автоматизации тестирования в нашей компании сильно изменился. Количество людей, активно вовлечённых в разработку тестов, увеличилось с десяти до 40 человек. А любая новая функциональность в приложениях теперь обязательно должна быть покрыта тестами до релиза.
В таких условиях нам очень важно разрабатывать тесты настолько быстро, насколько это возможно. И делать их при этом стабильными — чтобы поддержка не отнимала у нас много времени. Мы решили поделиться практиками, которые помогают нам ускорять разработку тестов и повышать их стабильность.
В подготовке текста мне помогал мой коллега Виктор Короневич: с этой темой мы вместе выступали на конференции Heisenbug.
В первой части статьи мы рассказали о роли автоматизации в наших процессах, деталях фреймворка и подробно разобрали три практики, которые мы применяем при создании автотестов. Во второй части мы будем разбираться с верификацией изменения состояния элементов, настройкой предусловий тестов, разработкой шагов для простых и сложных действий, а также с верификацией необязательных элементов (которую обязательно нужно делать :)).
Напомню, что примеры из обеих частей в равной степени актуальны для тех, кто только начинает внедрять автоматизацию тестирования в своём проекте, и тех, кто уже активно ею занимается.
Спойлер
В конце статьи будет ссылка на тестовый проект со всеми практиками.
Практика 4. Верификация изменения состояния элементов
Пожалуй, эта практика одна из самых важных в мобильной автоматизации в принципе, потому что в приложениях очень редко встречаются статические элементы, которые сразу же отображаются на экране и всегда на нём доступны. Чаще мы сталкиваемся с тем, что загрузка элементов занимает какое-то время.
Например, в случае медленного интернета элементы, получаемые с сервера, отображаются с существенной задержкой. И если мы попытаемся их проверить до того, как они появятся, тесты будут падать.
Таким образом, прежде чем начать проверять элементы, нам необходимо дождаться их появления на экране. Естественно, эта проблема не нова и существуют стандартные решения. Например, в Selenium это различные типы методов wait, а в Calabash — метод wait_for.
Зачем нам свой велосипед, или Почему мы отказались от стандартного решения
Когда мы начинали строить наш фреймворк автоматизации, мы использовали стандартный метод wait_for для ожидания появления или изменения состояния элементов. Но в какой-то момент столкнулись с проблемой. Иногда, раз в две-три недели, у нас зависали все тесты. Мы не могли понять, как и почему это происходит и что мы делаем не так.
После того как мы добавили логи и проанализировали их, оказалось, что эти зависания были связаны с реализацией метода wait_for, входящего в состав фреймворка Calabash. wait_for использует метод timeout модуля Ruby Timeout, который реализован на глобальном потоке. А тесты зависали, когда этот метод timeout использовался вложено в других методах: наших и фреймворка Calabash.
Например, рассмотрим прокрутку страницы профиля до кнопки блокировки пользователя.
def scroll_to_block_button
wait_for(timeout: 30) do
ui.scroll_down
ui.wait_until_no_animation
ui.element_displayed?(BLOCK_BUTTON)
end
end
Мы видим, что используется метод wait_for. Происходит прокрутка экрана вниз, потом ожидание окончания анимации и проверка отображения кнопки блокировки.
Рассмотрим реализацию метода wait_until_no_animation.
def wait_until_no_animation
wait_for(timeout: 10) do
!ui.any_element_animating?
end
end
Метод wait_until_no_animation реализован так же с wait_for. Он ждёт, когда на экране закончится анимация. Получается, что wait_for, вызванный внутри wait_for, вызывает другие методы. Представьте себе, что вызовы wait_for также есть внутри методов Calabash. С увеличением цепочки wait_for внутри wait_for внутри wait_for риск зависания увеличивается. Поэтому мы решили отказаться от использования этого метода и придумать своё решение.
Требования к нему совпадали с требованиями к стандартным решениям. Нам нужен был метод, который бы повторял проверку до тех пор, пока не выполнится заданное условие либо пока не истечёт отведённое время. Если проверка не проходит успешно за отведённое время, наш метод должен выбрасывать ошибку.
Сначала мы создали модуль Poll с одним методом for, который повторял стандартный метод wait_for. Со временем собственная реализация позволила нам расширять функциональность модуля по мере того, как у нас появлялась такая необходимость. Мы добавили методы, ожидающие конкретные значения заданных условий. Например, Poll.for_true и Poll.for_false явно ожидают, что исполняемый код вернёт true либо false. В примерах ниже я покажу использование разных методов из модуля Poll.
Также мы добавили разные параметры методов. Рассмотрим подробнее параметр return_on_timeout. Его суть в том, что при использовании этого параметра наш метод Poll.for перестаёт выбрасывать ошибку, даже если заданное условие не выполняется, а просто возвращает результат выполнения проверки.
Предвижу вопросы «Как это работает?» и «Зачем это нужно?». Начнём с первого. Если в методе Poll.for мы будем ждать, пока 2 станет больше, чем 3, то мы всегда будем получать ошибку по тайм-ауту.
Poll.for { 2 > 3 }
> WaitError
Но если мы добавим наш параметр return_on_timeout и всё так же будем ждать, пока 2 станет больше, чем 3, то после окончания тайм-аута, 2 всё ещё не станет больше, чем 3, но наш тест не упадёт, а метод Poll.for вернёт результат этой проверки.
Poll.for(return_on_timeout: true) { 2 > 3 }
> false
Зачем это нужно? Мы используем параметр return_on_timeout для верификации изменения состояния наших элементов. Но очень важно делать это аккуратно, так как он может скрывать реальные падения тестов. При некорректном использовании тесты будут продолжать выполняться, когда заданные условия не выполняются, в тех местах, где должны были бы выбросить ошибку.
Варианты изменения состояния элементов
А теперь перейдём к самому интересному — поговорим о том, как проверять различные изменения состояния и какие изменения состояния вообще существуют. Познакомьтесь с нашим объектом тестирования — чёрным квадратом:
Он умеет всего две вещи: появляться на экране и пропадать с экрана.
Первый вариант изменения состояния называется «Должен появиться». Он происходит в том случае, когда состояние 1 – на экране нет нашего объекта тестирования, а состояние 2 – он должен появиться.
Если он появляется, то проверка проходит успешно.
Второй вариант изменения состояния называется «Должен пропасть». Происходит он тогда, когда в состоянии 1 отображается наш объект тестирования, а в состоянии 2 его быть не должно.
Третий вариант не такой очевидный, как первые два, потому что в нём, по сути, мы проверяем неизменность состояния. Называется он «Не должен появиться». Это происходит, когда в состоянии 1 наш объект тестирования не отображается на экране и спустя какое-то время в состоянии 2 он всё ещё не должен появиться.
Вы, наверное, догадались, какой вариант — четвёртый. Он называется «Не должен пропасть». Происходит это, когда в состоянии 1 объект отображается на экране, и спустя какое-то время в состоянии 2 он всё ещё находится там.
Реализация проверок разных вариантов
Мы зафиксировали все возможные варианты изменения состояния элементов. Как же их проверить? Разобьём реализацию на проверки первых двух вариантов и проверки третьего и четвёртого.
В случае с первыми двумя вариантами всё довольно просто. Для проверки первого нам просто нужно подождать, пока элемент появится, используя наш метод Poll:
# вариант "Должен появиться"
Poll.for_true { ui.elements_displayed?(locator) }
Для проверки второго — подождать, пока элемент пропадёт:
# вариант "Должен пропасть"
Poll.for_false { ui.elements_displayed?(locator) }
Но в случае с третьим и четвёртым вариантами всё не так просто.
Рассмотрим вариант «Не должен появиться»:
# вариант "Не должен появиться"
ui.wait_for_elements_not_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not appear")
Здесь мы, во-первых, фиксируем состояние отсутствия элемента на экране.
Далее, используя Poll.for с параметром return_on_timeout, мы ждём появления элемента. При этом метод Poll.for не выбросит ошибку, а вернёт false, если элемент не появится. Значение, полученное из Poll.for, сохраняется в переменной actual_state.
После этого происходит проверка неизменности состояния элемента с использованием метода assert.
Для проверки варианта «Не должен пропасть» мы используем похожую логику, ожидая пропажи элемента с экрана вместо его появления:
# вариант "Не должен пропасть"
ui.wait_for_elements_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { !ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not disappear")
Проверки этих четырёх вариантов изменения состояния актуальны для многих элементов мобильных приложений. А поскольку разработкой тестов у нас занимается много людей, всегда есть вероятность того, что кто-то забудет про некоторые варианты при создании новых проверок. Поэтому мы вынесли реализацию проверок всех вариантов изменения состояния в один метод:
def verify_dynamic_state(state:, timeout: 10, error_message:)
options = {
return_on_timeout: true,
timeout: timeout,
}
case state
when 'should appear'
actual_state = Poll.for(options) { yield }
Assertions.assert_true(actual_state, error_message)
when 'should disappear'
actual_state = Poll.for(options) { !yield }
Assertions.assert_true(actual_state, error_message)
when 'should not appear'
actual_state = Poll.for(options) { yield }
Assertions.assert_false(actual_state, error_message)
when 'should not disappear'
actual_state = Poll.for(options) { !yield }
Assertions.assert_false(actual_state, error_message)
else
raise("Undefined state: #{state}")
end
end
yield – это код блока, переданного в данный метод. На примерах выше это был метод elements_displayed?. Но это может быть любой другой метод, результат выполнения которого отражает состояние необходимого нам элемента. Документация Ruby.
Таким образом, мы можем проверять любые варианты изменения состояния любых элементов вызовом одного метода, что существенно облегчает жизнь всем командам тестирования.
Выводы:
важно не забывать про все четыре варианта изменения состояния при проверках UI-элементов;
полезно вынести эти проверки в общий метод.
Мы рекомендуем использовать полную систему проверок всех вариантов изменения состояния. Что мы имеем в виду? Представьте, что когда элемент есть — это состояние true, а когда его нет — false.
Состояние 1 | Состояние 2 | |
Должен появиться | FALSE | TRUE |
Должен пропасть | TRUE | FALSE |
Не должен появиться | FALSE | FALSE |
Не должен пропасть | TRUE | TRUE |
Мы строим матрицу всех комбинаций. При появлении нового состояния таблицу можно расширить и получить новые комбинации.
Практика 5. Надёжная настройка предусловий тестов
Как можно догадаться из заголовка, задача этого раздела — разобраться, как настраивать предусловия перед началом выполнения теста.
Рассмотрим два примера. Первый – отключение сервиса локации на iOS в настройках. Второй – создание истории чата.
В первом примере реализация метода отключения сервиса локации на iOS выглядит следующим образом:
def switch_off_location_service
ui.wait_for_elements_displayed(SWITCH)
if ui.element_value(SWITCH) == ON
ui.tap_element(SWITCH)
ui.tap_element(TURN_OFF)
end
end
Мы ждём, пока переключатель (элемент switch) появится на экране. Потом проверяем его состояние. Если оно не соответствует ожидаемому, мы его изменяем.
После этого мы закрываем настройки и запускаем приложение. И иногда внезапно сталкиваемся с проблемой: почему-то сервис локации остаётся включённым. Как это получается? Мы же сделали всё, чтобы его отключить. Кажется, что это проблема работы системных настроек в iOS системе. При быстром выходе из настроек (а тест делает это моментально после нажатия на переключатель) их новое состояние не сохраняется. Но проблемы могут возникнуть и при настройке предусловий в нашем приложении.
Давайте обратимся ко второму примеру — созданию истории чата перед началом выполнения теста. Реализация метода выглядит следующим образом:
def send_message(from:, to:, message:, count:)
count.times do
QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
end
end
Мы используем QAAPI для отправки сообщений по user_id. В цикле мы отправляем необходимое количество сообщений.
После этого мы заходим в чат и производим необходимые проверки, связанные с загрузкой истории сообщений. Но иногда в чате отображаются не все сообщения. Иногда мы продолжаем получать их, уже находясь в чате, а не до его открытия. Связано это с тем, что сервер не может отправлять нужное количество сообщений моментально. В некоторых случаях доставка задерживается.
Выделим общую проблему обоих примеров. Когда установка предусловий выполняется некорректно, тесты падают на последующих проверках, несмотря на то, что сами приложения работают как надо. Мы начинаем разбираться в причинах, а потом понимаем, что падение вызвано неправильной подготовкой предусловий. Такие ситуации расстраивают не только тестировщиков, но и разработчиков, которые анализируют прогоны тестов при разработке новой функциональности.
Как же решить эту проблему? Мы можем добавить гарантию выполнения действия в методы установки предусловий.
Тогда наш метод отключения сервиса локации будет выглядеть следующим образом:
def ensure_location_services_switch_in_state_off
ui.wait_for_elements_displayed(SWITCH)
if ui.element_value(SWITCH) == ON
ui.tap_element(SWITCH)
ui.tap_element(TURN_OFF)
Poll.for(timeout_message: 'Location Services should be disabled') do
ui.element_value(SWITCH) == OFF
end
end
end
Используя метод Poll.for, мы убеждаемся, что состояние переключателя изменилось, прежде чем переходить к следующим действиям теста. Это позволяет избежать проблем, вызванных тем, что сервис локации время от времени был включён.
Во втором примере нам снова помогут наши методы QAAPI.
def send_message(from:, to:, message:, count:)
actual_messages_count = QaApi.received_messages_count(to, from)
expected_messages_count = actual_messages_count + count
count.times do
QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
end
QaApi.wait_for_user_received_messages(from, to, expected_messages_count)
end
Перед отправкой сообщений мы получаем текущее количество сообщений в чате, а после — убеждаемся, что необходимое количество сообщений было отправлено. Только после этой проверки тест продолжает своё выполнение. Таким образом, когда мы открываем чат в приложении, мы видим все необходимые сообщения и можем выполнять нужные проверки.
Итак, мы рекомендуем всегда использовать гарантию выполнения действий в процессе установки предусловий в ваших тестах. Это позволит сэкономить время на исследовании падений в тех случаях, когда не срабатывает установка предусловий, потому что тест упадёт сразу же на этапе подготовки.
В качестве общей рекомендации советуем добавлять в ваши тесты проверку выставления любых предустановок, когда выполняются асинхронные действия.
Более подробно о проблемах, описанных в этом разделе, можно прочитать в статье Мартина Фаулера.
Практика 6. Простые и сложные действия, или Независимость шагов в тестах
Простые действия
По выводам из предыдущего раздела может показаться, что нам стоит добавлять в тесты проверки выполнения всех действий. Но на деле это не так. В этом разделе мы поговорим о реализации шагов для разных действий и верификаций. Это очень общая формулировка, которая подходит практически для любого шага в наших тестах. Поэтому мы, как обычно, будем рассматривать конкретные примеры.
Начнём с теста поиска и отправки GIF-сообщений.
Сначала нам нужно открыть чат с пользователем, которому мы хотим отправить сообщение:
When primary_user opens Chat with chat_user
Потом открыть поле ввода GIF-сообщений:
And primary_user switches to GIF input source
Далее нам нужно ввести поисковый запрос для GIF-изображений, убедиться, что список обновился, отправить понравившееся изображение из списка и убедиться, что оно было отправлено.
And primary_user searches for "bee" GIFs
And primary_user sends 7th GIF in the list
Then primary_user verifies that the selected GIF has been sent
Целиком сценарий выглядит так:
Scenario: Searching and sending GIF in Chat
Given users with following parameters
| role | name |
| primary_user | Dima |
| chat_user | Lera |
And primary_user logs in
When primary_user opens Chat with chat_user
And primary_user switches to GIF input source
And primary_user searches for "bee" GIFs
And primary_user sends 7th GIF in the list
Then primary_user verifies that the selected GIF has been sent
Обратим внимание на шаг, который отвечает за поиск гифки:
And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
chat_page = Pages::ChatPage.new.await
TestData.gif_list = chat_page.gif_list
chat_page.search_for_gifs(keyword)
Poll.for_true(timeout_message: 'Gif list is not updated') do
(TestData.gif_list & chat_page.gif_list).empty?
end
end
Здесь, как и почти во всех остальных шагах, мы делаем следующее:
сначала ожидаем открытия нужной страницы (ChatPage);
потом сохраняем список всех доступных GIF-изображений;
далее вводим ключевое слово для поиска;
затем ждём изменения состояния — обновления списка (ведь мы говорили о том, что полезно добавлять в тесты проверку выполнения действий).
Кажется, что всё реализовано правильно. После завершения поиска мы убеждаемся, что список изображений обновился, и только после этого отправляем одно из них. Но у нас появится проблема, если мы, например, захотим написать тест, проверяющий, что после ввода идентичного поискового запроса список изображений не обновится. В этом случае нам придётся создавать отдельный шаг для ввода поискового запроса для GIF-изображений, который во многом будет дублировать уже имеющийся.
Подобная проблема возникает в тех случаях, когда какое-то действие может приводить к разным результатам, а мы в шаге фиксируем лишь один из них в виде проверки выполнения действия. Это приводит к сложности переиспользования такого рода шагов, и, следовательно, к замедлению разработки тестов и дубликации кода.
Как же нам этого избежать? Как вы, возможно, заметили, наш шаг поиска GIF-изображений на самом деле включал в себя три действия:
сохранение текущего списка;
поиск;
проверку обновления списка.
Решением проблемы переиспользования будет разделение этого шага на три простых и независимых.
Первый шаг сохраняет текущий список изображений:
And(/^primary_user stores the current list of GIFs$/) do
TestData.gif_list = Pages::ChatPage.new.await.gif_list
end
Второй шаг – поиск гифки — позволяет напечатать ключевое слово для поиска:
And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
Pages::ChatPage.new.await.search_for_gifs(keyword)
end
На третьем шаге мы ждём обновления списка:
And(/^primary_user verifies that list of GIFs is updated$/) do
chat_page = Pages::ChatPage.new.await
Poll.for_true(timeout_message: 'Gif list is not updated') do
(TestData.gif_list & chat_page.gif_list).empty?
end
end
В итоге наш первоначальный сценарий выглядит следующим образом:
Scenario: Searching and sending GIF in Chat
Given users with following parameters
| role | name |
| primary_user | Dima |
| chat_user | Lera |
And primary_user logs in
When primary_user opens Chat with chat_user
And primary_user switches to GIF input source
And primary_user stores the current list of GIFs
And primary_user searches for "bee" GIFs
Then primary_user verifies that list of GIFs is updated
When primary_user sends 7th GIF in the list
Then primary_user verifies that the selected GIF has been sent
Таким образом, мы можем использовать первые два шага даже в том случае, если в тесте список не обновляется. Это помогает нам экономить время на разработку, так как мы можем переиспользовать простые шаги в разных тестах.
Но и здесь есть нюансы. Шаги не всегда можно сделать простыми и независимыми. В таких случаях мы будем называть их сложными.
Сложные действия
Под сложными шагами мы подразумеваем те, которые включают в себя переходы между разными экранами приложения либо работу с изменением состояния какого-либо экрана. Рассмотрим такую ситуацию на примере голосования в мини-игре.
Мини-игра — это экран, на котором пользователю предлагаются профили других людей, которые ему написали. Можно либо отвечать на сообщения, либо пропускать этих пользователей. Действие пропуска назовём «Голосовать “нет”».
Нам необходимо написать тест, который «проголосует “нет”» N раз, закроет экран игры, а потом откроет его снова и проверит, что пользователь находится на правильной позиции.
«Проголосовать “нет”» — простое действие. Но, если мы сделаем для него простой шаг, то для того чтобы проголосовать N раз, нам нужно будет использовать этот шаг столько же раз на уровне сценария. Читать такой сценарий неудобно. Поэтому есть смысл создать более сложный шаг с параметром «Количество голосов», который сможет проголосовать необходимое нам количество раз.
Кажется, что реализовать такой шаг довольно легко. Необходимо всего лишь проголосовать на странице нужное количество раз.
When(/^primary_user votes No in Messenger mini game (\d+) times$/) do |count|
page = Pages::MessengerMiniGamePage.new.await
count.to_i.times do
page.vote_no
end
end
Но тут мы сталкиваемся с проблемой: иногда тест голосует слишком быстро. То есть он нажимает на кнопку до того, как обрабатывается предыдущее нажатие. В этом случае приложение не отправит новый голос на сервер. Наш шаг при этом выполнится успешно. А на следующем шаге, когда мы захотим убедиться, что пользователь находится на правильной позиции в игре, тест упадёт, потому что предыдущий шаг не выполнил свою задачу и сделал меньше голосов, чем было нужно.
Как и в случае с установкой предусловий теста, здесь нам придётся разбираться с падениями на том шаге, где тест и приложение работают корректно, поскольку предыдущий шаг сработал неправильно. Конечно же, никто не любит такие ситуации, поэтому нам необходимо решить проблему.
When(/^primary_user votes No in Messenger mini game (\d+) times$/) do |count|
page = Pages::MessengerMiniGamePage.new.await
count.to_i.times do
progress_before = page.progress
page.vote_no
Poll.for_true do
page.progress > progress_before
end
end
end
После выполнения каждого голоса мы добавим проверку изменения прогресса в мини-игре. Только после этого мы будем делать следующую попытку проголосовать. Так все голоса будут успевать обрабатываться, а мы избежим падений наших тестов.
Подведём итоги. Мы создаём независимые шаги для простых действий. Это позволяет легко их переиспользовать и создавать различные сценарии за более короткое время, чем если бы нам нужно было переписывать похожие друг на друга шаги. В шаги для сложных действий мы добавляем проверки того, что действие выполнено, перед переходом к следующим действиям.
Обобщая эти рекомендации, советуем выделять независимые методы для простых действий в тестах и добавлять проверку предустановок в сложные действия.
Практика 7. Верификация необязательных элементов
Под необязательными элементами мы понимаем такие элементы, которые могут либо отображаться, либо не отображаться на одном и том же экране в зависимости от каких-либо условий. Здесь мы рассмотрим пример диалогов о подтверждении действий пользователя, или алёртов (alerts).
Наверняка вы сталкивались с подобными диалогами в различных мобильных приложениях. В наших двух приложениях, например, их больше 70. Они появляются в разных местах в ответ на разные действия пользователей. Что же представляют собой необязательные элементы на них?
Проанализируем скриншоты выше.
Скриншот 1: заголовок, описание и две кнопки.
Скриншот 2: заголовок, описание и одна кнопка.
Скриншот 3: описание и две кнопки.
Таким образом, необязательными элементами в этих диалогах являются заголовок и вторая кнопка. Локаторы у элементов на алёртах одинаковые — вне зависимости от того, в каком месте приложения или после какого действия пользователя они появляются. Поэтому мы хотим реализовать проверки для всех типов диалогов один раз на их базовой странице, а потом наследовать от неё каждый конкретный алёрт, чтобы не пришлось повторять этот код. Создание подобных проверок мы и рассмотрим в этом разделе.
Начнём с того, как выглядит вызов метода для верификации каждого из диалогов:
class ClearAccountAlert < AppAlertAndroid
def verify_alert_lexemes
verify_alert(title: ClearAccount::TITLE,
description: ClearAccount::MESSAGE,
first_button: ClearAccount::OK_BUTTON,
last_button: ClearAccount::CANCEL_BUTTON)
end
end
class WaitForReplyAlert < AppAlertAndroid
def verify_alert_lexemes
verify_alert(title: WaitForReply::TITLE,
description: WaitForReply::MESSAGE,
first_button: WaitForReply::CLOSE_BUTTON)
end
end
class SpecialOffersAlert < AppAlertAndroid
def verify_alert_lexemes
verify_alert(description: SpecialOffers::MESSAGE,
first_button: SpecialOffers::SURE_BUTTON,
last_button: SpecialOffers::NO_THANKS_BUTTON)
end
end
Во всех примерах мы вызываем метод verify_alert, передавая ему лексемы для проверки необходимых элементов. При этом, как вы можете заметить, WaitForReplyAlert мы не передаём лексему для второй кнопки, так как её не должно быть, а SpecialOffersAlert — лексему для заголовка.
Рассмотрим реализацию метода verify_alert:
def verify_alert(title: nil, description:, first_button:, last_button: nil)
ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title
ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
ui.wait_for_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON) if last_button
end
Сначала мы ожидаем появления на экране обязательных элементов. После этого убеждаемся, что их текст совпадает с переданными в метод лексемами. В случае же с необязательными элементами мы проверяем текст, если лексема была передана в метод, а если нет, то не делаем ничего.
В чём же проблема этого подхода? В том, что мы пропускаем проверку того, что необязательный элемент не отображается тогда, когда он не должен отображаться. Это может привести к комичным ситуациям. Например, может появиться такой алёрт:
Пользователь не понимает, что выбрать: обе кнопки как будто бы закрывают диалог. Это похоже на критический баг. Даже если приложение не падает, это нужно исправить. А тесты нам нужно изменить так, чтобы они выявляли подобные проблемы.
Для этого в тестах мы меняем проверку
ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title
на
if title.nil?
Assertions.assert_false(ui.elements_displayed?(ALERT_TITLE), "Alert title should not be displayed")
else
ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE)
end
Мы изменили условие if и добавили проверку второго состояния. Если мы не передаём лексему для необязательного элемента, значит, этого элемента не должно быть на экране, что мы и проверяем. Если же в title есть какой-то текст, мы понимаем, что элемент с этим текстом должен быть, и проверяем его. Мы решили выделить эту логику в общий метод, который назвали wait_for_optional_element_text. Этот метод мы можем применять не только для диалогов из этого примера, но и для любых других экранов приложения, на которых есть необязательные элементы. Видим, что if-условие из примера выше полностью находится внутри нового метода:
def wait_for_optional_element_text(expected_lexeme:, locator:)
GuardChecks.not_nil(locator, 'Locator should be specified')
if expected_lexeme.nil?
Assertions.assert_false(elements_displayed?(locator), "Element with locator #{locator} should not be displayed")
else
wait_for_element_text(expected_lexeme: expected_lexeme, locator: locator)
end
end
Реализация метода verify_alert тоже изменилась:
def verify_alert(title: nil, description:, first_button:, last_button: nil)
ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
ui.wait_for_optional_element_text(expected_lexeme: title, locator: ALERT_TITLE)
ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
ui.wait_for_optional_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON)
end
Мы всё так же ожидаем появления обязательных элементов на экране. После этого проверяем, что их текст совпадает с переданными в метод лексемами. В случае же с необязательными элементами теперь мы используем метод wait_for_optional_element_text, который помогает убедиться, что необязательный элемент не отображается тогда, когда не должен отображаться.
Подводя итоги, хотим обратить ваше внимание на то, что не бывает необязательных проверок, — бывают необязательные элементы, все состояния которых нужно обязательно проверять. А выделение проверки состояний необязательных элементов в общий метод позволяет нам легко переиспользовать его для разных экранов приложения.
Резюме: мы советуем использовать полную систему проверок всех состояний нужных элементов, а также выделять общие методы для однотипных действий.
Вы можете заметить, что подобные рекомендации уже звучали в этой статье. Фокус в том, что различались примеры их применения.
Общие рекомендации
Выделим основные рекомендации по автоматизации тестирования мобильных приложений из семи практик, которые мы описали:
так как проверки – это то, ради чего мы пишем тесты, всегда используйте полную систему проверок;
не забывайте добавлять проверку предустановки для асинхронных действий;
выделяйте общие методы для переиспользования однотипного кода — как в шагах, так и в методах на страницах;
делайте объект тестирования простым;
выделяйте независимые методы для простых действий в тестах.
Возможно, эти советы кому-то покажутся очевидными. Но мы хотим обратить ваше внимание на то, что применять их можно (и нужно) в разных ситуациях. Если вы хотите дополнить список другими полезными рекомендациями, добро пожаловать в комментарии!
Бонус
Мы подготовили тестовый проект, в котором отразили все практики, описанные в статье. Переходите по ссылке, изучайте и применяйте: