Ранее мы с вами познакомились с пирамидой тестирования и ее основанием. В данной же статье предлагаю перейти к сразу к вершине пирамиды.
На вершине пирамиды, представленной в статье 1, расположены сквозные тесты. В контексте сквозных тестов, речь может идти об e2e (end-to-end), UI, системных, тестах пользовательского интерфейса... Иными словами в данной статье речь пойдет о тестах, которые проводятся над системой, как над единым целым. Основная задача этой группы тестов - проверка того, удовлетворяет ли вся система, как единое целое, представленным и заявленным требованиям.
В разработке мобильных приложений в целом, как и в разработке приложений под android в частности, понятие e2e тесты часто используется как синоним понятий тесты пользовательского интерфейса (UI тесты) и инструментальные тесты. Однако следует помнить, что в общем случае это не совсем так, поскольку они не всегда означают одно и то же, так как все может зависеть от контекста.
Инструментальные тесты – это тесты, для выполнения которых требуется специальная среда – либо физически подключенное устройство (смартфон, планшет и т.п.), либо эмулятор (симулятор). Данная группа может не ограничиваться привычными UI тестами, но может также включать в себя большое множество других видов тестирования - например тестирование работы с базой данных или диском, в которых UI вовсе не нужен, screenshot тесты и т.д.
E2E (сквозные) тесты, в свою очередь могут включать в себя:
-
тесты, в которых используется вся система целиком: все мобильное приложение, без подмены тех или иных слоев (к примеру в зависимости от выбранной тестовой стратегии и нужд команды, инструментальные тесты могут проводиться над приложением, c упрощенной логикой, без использования реального сетевого слоя и т.п.). Иными словами E2E тесты в контексте архитектуры приложения (сервиса) (штриховкой обозначены возможные виды тестирования и возможные зоны их применения):
-
тесты основных сценариев использования - от начала и до конца. Данный термин часто применяется, если пользовательский сценарий включает в себя слишком много шагов. Например, основной сценарий приложения онлайн магазина можно разбить на несколько слабо связанных этапов: авторизация, выбор товара, оплата, постоплата. Либо же можно объединить все этапы вместе и протестировать весь сценарий от начала и до конца (end-to-end).
Таким образом, при использовании данных терминов всегда следует помнить о контексте. Далее в данной статье в основном речь пойдет об инструментальных UI тестах.
Итак, UI тесты, как подмножество инструментальных тестов, выполняются на устройствах с операционной системой таргетируемой платформы (реальной или эмулированной), благодаря чему в них может использоваться практически все, что предоставляет API платформы.
UI тесты для мобильных приложений - это автотесты, симулирующие взаимодействие пользователя с приложением. Эта группа тестов позволяет проконтролировать, что элементы пользовательского интерфейса работают так, как предполагает сценарий.
Чаще всего такие тесты пишутся с использованием фреймворков. Для Android в основном это Espresso и UI Automator. Однако так же бывают и свои проприетарные решения с использованием сторонних технологий (например Jest). API фреймворков может быть не всегда удобным и читаемым в силу ряда причин, и к ним зачастую пишутся свои обертки и библиотеки для более удобной работы (например Kakao, Kaspresso).
Наиболее распространенной практикой при написании UI тестов является применение паттерна Page Object. Основная идея подхода заключается в создании объектно-ориентированного представления экрана/страницы (изначально паттерн применялся при тестировании web-страниц), с которой и взаимодействует тест. Такое представление позволяет отвязать тесты от деталей реализации страницы и писать более качественные тесты, которые намного проще писать и поддерживать. Пример такого представления для приложения, описанного в статье 1:
class LocationSelectionScreen : Screen<LocationSelectionScreen>() {
val nextButton = KView { withText(R.string.next_action_bar_button) }
val locationsList =
KRecyclerView(
builder = { withId(R.id.recycler_view) },
itemTypeBuilder = { itemType(LocationSelectionScreen::LocationItem) }
)
class LocationItem(parent: Matcher<View>) : KRecyclerItem<LocationItem>(parent) {
val label: KTextView = KTextView(parent) { withId(R.id.item_label) }
}
}
Пример самого теста:
@Test
fun testLocationSelection() {
onScreen<LocationSelectionScreen> {
locationsList {
isVisible()
firstChild<LocationSelectionScreen.LocationItem> {
isVisible()
label { hasText("Sydney") }
click()
}
}
nextButton.click()
}
onScreen<ForecastScreen> {
screenView {
isVisible()
}
locationName {
hasText("Sydney")
}
}
}
Пример запуска:
Пользу данного подхода невозможно переоценить, в случае изменения интерфейса экрана. В таком случае будет необходимо лишь изменить один page object вместо переписывания всех существующих тестов. Как видим, сам тест не привязан к конкретной реализации экрана: locationsList
может быть как RecyclerView
, ListView
так и своим проприетарным решением. А в случае миграции с одной реализации на другую, скорее всего придется поменять лишь LocationSelectionScreen
, а тесты оставить без изменения.
К тому же, если такие представления будут написаны аналогичным образом на других платформах (iOS/Web) можно прибегнуть к автоконвертации кода, что позволит QA-инженеру переиспользовать тесты между платформами и многократно повысят их продуктивность.
Как можно заметить, в противоположность к unit-тестам, тесты пользовательского интерфейса, и тем более e2e тесты, включают в себя большое количество компонентов и систем. Что с одной стороны также позволяет нам протестировать все эти системы и их взаимодействие написав лишь один тест. Однако из этого также вытекает и их основной недостаток - из-за наличия большого количества компонентов, которые могут влиять на результат выполнения – такие тесты не всегда стабильны.
Объяснить их нестабильность довольно просто классическим инженерным подходом.
Подобными вопросами занимается такая область науки как Теория надёжности, которая определяет надежность, как свойство объекта (системы) сохранять во времени в установленных пределах значения всех параметров, характеризующих способность выполнять требуемые функции в заданных условиях применения, технического обслуживания, хранения и транспортирования.
Если для простоты принять, что каждая команда в нашей системе проходит по системе с последовательным соединением (в реальности все конечно же намного сложнее): эмулирование нажатия на экран, обработка нажатия операционной системой, обработка нажатия кодом приложения, сетевой запрос/ответ, обработка ответа приложением, выработка команды обновления пользовательского интерфейса приложения, обработка OS полученной команды, аппаратное обновление интерфейса, обработка измененного состояния интерфейса тестовым фреймворком… То можно применить формулу [1] умножения вероятностей:
То есть надежность нашей системы есть произведение надежностей элементов ее составляющих. Идеальных же компонентов не существует, и если предположить что наша система состоит из 10 элементов, надежность каждого из которых 99%, получим
Что можно трактовать так, что каждый десятый запуск теста будет заканчиваться неудачей. Из-за чего команде придется часто проверять, реально ли что-то сломалось, либо же результат ложноотрицательный. Что, согласитесь, весьма плохо и будет невероятно сильно тормозить разработку и демотивировать команду (мы ведь все хотим писать новые фичи, которые мы можем добавить в наше performance review, а не править фантомные баги).
Помимо нашего кода, на результат выполнения теста также сильное влияние может оказывать его окружение – состояние эмулятора, либо устройства, состояние сети (если она используется). Также выполнение UI тестов невероятно медленное и ресурсоемкое. К примеру выполнение теста приведенного выше на моем устройстве заняло 2 секунды, в то время как прогон unit тестов занимает доли секунды.
Как можно заметить, проблем с данным видом тестирования хватает, но не стоит спешить списывать их со счетов. Существуют способы и решения, позволяющие минимизировать упомянутые проблемы.
Предположим стабильность теста составляет 90%, тогда в среднем 1 запуск из 10 будет давать ложноотрицательный результат. Данный результат не выглядит таким уж радужным. Качество такого сигнала будет довольно низким, и в какой-то момент команда может просто перестать обращать на это внимание или вовсе отключить его. На помощь опять может прийти формула [1]. Если тест падает в 10% случаев и эти события не связаны между собой - можно перезапускать его после первого падения и учитывать результат 2 запусков. Если для нас достаточно чтобы тест прошел хотя бы 1 раз, то в таком случае мы увидим красный
результат в нашем отчете довольно реже: 0.1x0.1=0.01 или 1% (в случае 95% стабильности, получим еще более приемлемый результат: 0.05x0.05=0.0025 или 0.25%). Существуют проекты, которые позволяют решить эту и ряд других проблем (например Marathon).
Как видно, UI тесты - довольно мощный инструмент, однако пользоваться им следует очень аккуратно. За время работы с ними, накопился следующий список замечаний:
В зависимости от размера команды и проекта, от количества самих тестов - можно варьировать стратегии, по которым производится запуск тестов. Например, можно выработать стратегию на основе impact анализа и проверять только те части проекта, которые были задеты изменениями (маловероятно, что что-то сломается на экране выбора местоположения, если изменить способ округления температуры на следующем экране).
При большом количестве разработчиков и большой активности в проекте - может быть более выгодно не запускать тесты на каждое изменение, а запускать их через какие-то интервалы времени или после какого-то определенного количества внесенных изменений. И в случае регрессии уже ретроспективно искать изменение, которое ее вызвало (например с помощью механизма bisect).
UI тесты могут позволить стабилизировать разработку на начальных этапах нового проекта, когда требования могут активно меняться или проект может быть даже отменен. Также часто бывает, что времени не хватает даже на минимально жизнеспособный продукт, не то что на тесты. В такие моменты можно написать пару UI тестов, которые покроют основные сценарии и дадут хоть какое-то представление о том, что происходит с проектом. Мы конечно же можем сказать нет! продуктовой команде, но всегда следует брать во внимание также и внешние факторы. Бывают моменты, когда лучше выпустить продукт без достаточного тестового покрытия, чем успеть написать только половину проекта, идеально его протестировать, но так и не дописать вторую часть, потому что не удалось получить новое финансирование или динамика не удовлетворила топ менеджмент и проект просто закрыли. (К сожалению приходилось наблюдать подобные сценарии развития событий даже на практике).
Можно выработать стратегию, при которой будет сочетаться несколько типов тестирования, каждый из которых будет ответственен за свою определенную часть, с которой он справляется лучше всего. На одном из проектов мы пришли к тому, что UI тесты были ответственны за тестирование работоспособности навигации и корректному отображению UI (без проверки конкретных значений). Также небольшое количество e2e тестов позволяли следить за общим здоровьем проекта.
Подытожим, тесты пользовательского интерфейса:
позволяют удостовериться, что основная функциональность приложения работает корректно;
позволяют выявить ошибки в работе элементов пользовательского интерфейса;
позволяют сэкономить время (их запуск гораздо быстрее ручного тестирования);
повышают тестовое покрытие и позволяют численно следить за состоянием проекта;
ресурсоемки - их создание и поддержание, могут требовать специальных знаний и инструментов;
затратны по времени - создание, запуск и отладка могут требовать значительные временные затраты (по сравнению с другими видами автоматизированного тестирования);
хрупкие - их стабильность зависит от стабильности большого количества компонентов и систем;
ограничены - позволяют тестировать лишь функциональность, которая влияет на изменение пользовательского интерфейса.
Надеюсь, удалось остаться до конца объективным и раскрыть как положительные, так и отрицательные моменты тестов, относящихся к верхнему уровню пирамиды. Остались еще моменты и наблюдения, относящиеся UI тестам, которые планирую раскрыть в последующих статьях про интеграционные и контрактные тесты.