Далее расшифровка выступления Михаила Домрачева на AppsConf 2017, в ходе которого он рассказал, как на практике внедрить UI-тесты в iOS проектах, и поделился мыслями, когда это действительно необходимо, а когда — излишне.
Забегая вперед, отметим, что тут есть и плюсы, и минусы. Но, на наш взгляд, существенное уменьшение количества дизайн-багов без огромных трудозатрат ручного тестирования — неоспоримое преимущество, которое любого должно примерить с небольшими возникающими трудностями.
Бэкраунд
Начнем издалека, с описания проекта до того, как мы столкнулись с проблемами с дизайном.
- Проект активно живет и развивается 2 года, его можно назвать долгосрочным.
- Команда разработчиков увеличивается.
- Проект состоит из множества мобильных приложений, которые имеют общую функциональность.
- Разработан Core framework, в который выделена общая функциональность, и который подключается к каждому проекту в видеподмодуля.
Мы работаем с довольно большой зарубежной финансовой компанией, у которой есть филиалы в разных странах. В каждой стране есть свое мобильное приложение, которое имеет общие функции, но отличается дизайном и некоторой специфичностью для страны, связанной, например, с законодательством.
Техническая сторона
Все наши экраны реализованы в табличном виде. Таблица состоит из набора таких связок, как ячейка и View-Model для нее. То есть мы можем настроить контент ячейки через View-Model и выставить определенный дизайн, в разных странах она будет выглядеть по-разному, но по сути это один и тот же ресурс, который лежит в Core framework.
На рисунке показан экран. В верхней ячейке есть своя View-Model, через которую можно:
- настроить background color,
- выставить название картинки (imageName), которую берем из ресурсов.
Во View-Model для кнопки можно выставить:
- контент (title),
- дизайн (title color),
- background color этой кнопки.
Core framework
В среднем в каждом новом приложении у нас переиспользуется примерно 70% функциональности из Core framework и только 30% накручивается чего-то нового.
В Core framework находится:
- Screen module — уже готовые экраны в виде модулей. Я их называю модулями, потому что там уже реализована некая стандартная бизнес-логика, роутинг перехода на другие экраны и имеется свой дизайн по умолчанию.
- Any services — различные базовые сервисы, например, для работы с сетью, с кэшированием данных и т.д.
- Utils — это категории (хэлперы), которые помогают быстрее писать код.
- Design elements — все дизайн-элементы, которые состоят из:
- xib для всех ячеек:
- cell — классы для этих ячеек
- View-Model.
В каждом новом приложении нам приходится добавлять следующее:
- Override screen module — переопределение экранов из Core framework.
- New screen modules —новые экраны, если эти функции нигде не будут переиспользованы.
- Override services — обычно нужно переопределить Network-сервис, потому, что API во всех странах работают не на 100% идентично.
- xib — иногда необходимо разработать новый дизайн для ячейки.
Хотя обычно все ячейки берутся из Core. Их дизайн мы можем настроить через View-Model, они довольно универсальные, и на выходе мы получаем дизайн в виде Лего.
В результате у нас получается такой «Франкенштейн».
Примеры
Это один и тот же экран, но в разных странах. Он имеет общую базовую реализацию, то есть запрограммирован в Core. Отличия в дизайне мы реализуем через различные категории для цветов и картинок. Если приглядеться, тут с трудом можно найти 10 отличий, например:
- разные картинки, в разных странах в ресурсах находятся разные картинки с одним и тем же именем;
- разный фон у калькулятора;
- различное выравнивание информации и немного другие шрифты;
- разный цвет у кнопки
На втором типе экрана уже посложнее. Слева — базовая реализация в Core framework, справа — заказчик в новой стране решил немного по-другому отображать ту же самую информацию. Бизнес-логика сохранилась, но надо было сделать новый дизайн. Мы использовали другие ячейки (тоже из Core framework), и также реализовали новую функциональность — оплату кредита банковской картой.
Все вроде было хорошо, заказчик был очень доволен и предложил увеличить обороты и делать новые страны быстрее. Но, естественно, мы не все учли.
Источники проблем
- Новые разработчики
Чтобы увеличить обороты на проекте, были наняты новые разработчики.
- Общие ресурсы
Как уже говорил, у нас общие ресурсы для всех стран, и они в открытом доступе. Новый разработчик (или даже старый) мог модифицировать параметры или двигать/менять элементы в общих ресурсах. Все, конечно, понимают, что так нельзя делать, но все равно бывали случаи.
- Дизайнер забыл про единый стиль приложений
Все наши приложения имеют единый стиль, например, одинаковое выравнивание текста, определенные цвета ячеек, одинаковые фоны. Со временем, наш дизайнер начал забывать про этот единый стиль и мог внести изменения в стиль базовой ячейки. Но он даже не подозревал, что тем самым может навредить совсем другому приложению, за которое он даже не в ответе. А разработчик, который им занимается, тоже не знал, что ему навредили. В итоге при ручном тестировании постоянно находились какие-то проблемы с «уехавшим» дизайном.
Мы сразу поняли, что решением этих проблем будет внедрение Ul-тестов. Перед тем, как внедрять Ul-тесты, мы сделали небольшой анализ рынка инструментов, с помощью которых это можно делать именно на iOS проектах, другие платформы мы не рассматривали.
Инструменты Ul-тестирования:
- iOS Ul testing от Apple;
- appium — сейчас самый популярный фреймворк для написания единых тестов для разных ОС. Можно написать общий тест и экспортировать его в разные платформы.
- Calaba.sh — открытый фреймворк, разработанный Xamarin, также подразумевает написание общих тестов для разных платформ.
Мы сделали анализ по четким критериям:
- Стоимость. Все наши продукты в базовой комплектации без всяких фич бесплатны, любой разработчик может их прикрутить к своему проекту и пользоваться.
- Кроссплатформенность. Нам это было не так важно, но при анализе все равно рассматривали этот критерий.
- Кодогенерация, то есть возможность test-recording — во время взаимодействия с UI элементами генерируется код нашего теста.
У Apple и Appium есть такие инструменты, а у Calaba.sh в базовой комплектации нет, но есть Xamarin cloud, где эту функцию можно купить. Также там предоставляется более 2000 девайсов, на которых можно прогонять тесты. Test-recording — не всегда очень хорошо. Когда мы руками ничего не пишем, мы иногда не понимаем, что генерируется. Отсутствие такого инструмента может сыграть и в плюс. Об этом я расскажу чуть позже.
4. Опыт использования членами нашей команды какого-либо инструмента до нашего проекта.
Все ребята в нашей команде использовали только один инструмент — Apple. Точнее, просто что-то знали про него. Все единогласно проголосовали за этот инструмент. В принципе, нам хватало тех функций, которые он дает, другие платформы нам были не так важны.
Расскажу про него немного поподробнее.
iOS Ul testing — довольно молодая технология, которая была представлена в 2015 году. Она основывается на двух вещах:
- XCTest — фреймворк, который позволяет писать и запускать тесты;
- Accessibility — позволяет людям с ограниченными возможностями пользоваться устройствами Apple в полной мере. Для UI-тестов он предоставляет возможность взаимодействовать с UI элементами во время выполнения UI-тестов.
XCTest базируется всего на трех классах:
- XCUI Application — своего рода прокси между приложением и тестами;
- XCUIEIement — ваш UI элемент на экране;
- XCUIEIementQuery — запрос на поиск элемента, который находится на экране по какому-то критерию.
Демо UI test recording
Все просто — ставим курсор в функцию, нажимаем record. Запускается UI-тест. Мы нажимаем на элементы и генерируется код.
Посмотрим, что он сгенерировал.
XCUIApplication *app = app2;
[app.buttons[@"start"] tap];
XCUIElement *element = [[[[app.otherElements
containingType:XCUIElementTypeNavigationBar identifier:@“UIView”]
childrenMatchingType:XCUIElementTypeOther].element
childrenMatchingType:XCUIElementTypeOther].element
childrenMatchingType:XCUIElementTypeOther].element;
[element tap];
XCUIApplication *app2 = app;
[app2.keys[@"t"] tap];
[app typeText:@“t"];
. . .
[app typeText:@"c"];
[app2.keys[@“o"]
[element tap];
Конечно, не всегда он делает так плохо, но я честно попытался 3-4 раза записать демо, и всегда натыкался на одни и те же ошибки:
- Неправильно инициализирован XCUI Application, не сделал log и init для него, а приравнял какой-то мифической переменной app2, которая до этого нигде не была определена. На следующем экране он сделал то же самое.
- Элемент найден неправильно. У элемента был выставлен AccessibilityNotifier, по которому он и должен был искать этот элемент. Но он этого не увидел, и стал искать просто через childrenMatchingType.
Минусы UI test recording:
- много кода;
- не всегда читаемый код — раз его много;
- не всегда работающий код;
- не реализуется переиспользование функциональности.
Эти минусы UI test recording не всегда встречаются, но при написании UI-тестов важна скорость. Хочется быстро, красиво написать какие-нибудь функции, и при этом не хочется возвращаться и что-то править после test recording. Обычно это занимает больше времени, чем сразу написать самому.
От теории к практике
Первое, с чем мы столкнулись, это Stub manager, вернее, с его отсутствием. До этого у нас на проекте не были внедрены unit-тесты, и мы никак не эмулировали связь с сетью.
Мы хотели не повторять ошибок test-recording и писать тесты более чистыми и правильными. В этом нам помог Page object pattern. Про него мало кто знает, но тестировщики, которые пишут тесты, должны быть в курсе.
Мы внедрили инструмент Snapshot для получения скриншотов во время выполнения наших UI-тестов, чтобы не только видеть, упал ли наш тест при его выполнении, но и смотреть результат, сравнивать дизайн на экранах.
Рассмотрим подробно каждый пункт.
Stub manager
Почему-то считается, что реализовать эмуляцию сети сложно. На самом деле все делается очень просто с помощью class NSURLProtocol. Это такой класс, который позволяет предопределить работу системы загрузки URL для iOS.
Все делается в два действия:
- создание своего класса, который наследуется от NSURLProtocol;
- регистрация его в системе загрузки URL.
Когда в приложении вы отправляете запрос в сеть, система загрузки сначала проверяет, зарегистрированы ли какие-либо свои NSURLProtocol’ы. Если да, то она к каждому из них в своем порядке обращается и спрашивает, возможно ли обработать данный запрос. Если мы говорим да, то запрос в сеть обрывается, и мы обрабатываем его на уровне приложения.
Кастомный class NSURLProtocol
Нужно переопределить всего три метода:
- можем ли мы обработать данный запрос (просто возвращается true/false):
+(BOOL)canInitWithRequest:(NSURLRequest *)request
- сам метод startLoading, где происходит вся магия подмены данных:
-(void)startLoading
- в конце говорим системе, что мы закончили обработку запроса:
-(void)stopLoading
Немного подробнее про startLoading, где происходит вся магия.
(void)startLoading
NSData *cachedData
if (cachedData) {
NSHTTPURLResponse *response
[self.client URLProtocol: didReceiveResponse: cacheStoragePolicy:];
[self.client URLProtocol:self didLoadData: cachedData];
[self.client URLProtocolDidFinishLoading: self];
} else {
[self.client URLProtocol: self
didFailWithError: создаем ошибку
}
Внутри мы просто берем данные из кэша
(NSData *cachedData)
. В нашем случае они хранятся в разных бандлах, потому что для каждого тест-кейса на один и тот же запрос могут быть разные ответы. Если такие данные есть, создаем свой кастомный response, отдаем его системе, и также говорим, чтобы она вернула закэшированные данные.
Если же мы не нашли этих данных, мы создаем просто свою кастомную ошибку, которая вернется на запрос, как будто она пришли из сети.
В Stub manager вы только регистрируете свой класс. Это делается через:
NSURLSessionConfiguration setProtocolClasses: @[[MyNSURLProtocol class]]
Все делается очень быстро, легко и работает. Так что не надо этого бояться.
Page object pattern
Как я говорил, мы решили побороться с минусами test-recording и более ответственно отнестись к написанию UI-тестов. Мы попытались сделать код более чистым, и применили паттерн Page object pattern.
Он довольно простой и основывается на том, что между тестами и экраном, для которого пишутся тесты, должна быть какая-то прослойка, где реализована переисиользуемая функциональность. То есть там зашиты все сложные взаимодействия с элементами и все поиски элементов на экранах.
Польза от Page object pattern:
- Читабельность кода.
- Переиспользование функциональности.
- Централизация интерфейса пользователя.
Наш подход
Мы решили, что будет излишним писать прослойку для каждого экрана. Мы написали просто общую прослойку для всего приложения, называется она XCTestUtils и состоит из 3 компонентов:
- Класс FFElements, в котором зашит поиск элементов на экране. В нем реализовано две функции: поиск по ключу и поиск по индексу.
@interface FFElements : NSObject -(XCUIElement *)objectForKeyedSubscript:(NSString *)key; -(XCUIElement *)objectAtIndexedSubscript:(NSUInteger)index; @end
- Категория к XCTestCase (Elements). Здесь мы указали определенные FFElements, которые нам нужны для написания UI-тестов: ячейки, лейблы, кнопки; текстовые поля.
@interface XCTestCase (Elements) @property (nonatomic,readonly) FFElements *textFields; @property (nonatomic,readonly) FFElements *buttons; @property (nonatomic,readonly) FFElements *labels; @property (nonatomic,readonly) FFElements *cells; -(void)wait:(NSTimeInterval)interval; @end
- Небольшая категория для XCUIEIement, которая помогает работать с textFields:
@interface XCUIElement (Utils) @property (nonatomic) NSString *pasteText; +(void)forceTap; @end
Как вы видели в демо, чтобы ввести какой-то текст в textFields, генерируется код нажатия на каждую кнопку. Мы ввели функцию forceTap — длительное нажатие. Когда мы нажимаем на textFields, у нас появляется плашка «Вставить текст» либо «Скопировать».
Получилось круто, и все были довольны.
Демо
Покажу небольшое демо о том, как мы пользуемся Page object pattern.
Через property FFElements обращаемся к конкретным элементам с определенным типом и вызываем методы для взаимодействия с ними. С textFields работаем так: нажимаем, делаем длительное нажатие, и вставляем текст. Получилось всего 6 строк — понятных, читаемых, и, главное, работающих.
Snapshot
Мы запускаем UI-тест, он прогоняется на устройстве, а мы смотрим, чтобы дизайн не поехал. А можем и просмотреть.
Поэтому мы решили внедрить такой инструмент, как Snapshot. Он входит в группу инструментов Fastlane, он довольно простой, легко внедряется и от него большая польза.
Польза от Snapshot:
- делает скриншоты;
- запускает UI-тесты и делает скриншоты в нужных местах;
- легко интегрируется с CI, потому что он запускается из терминала.
Для внедрения Snapshot требуется буквально три действия.
- В терминале вводим команду fastlane snapshot init. После этого он генерирует файл под названием Snapfile — это файл настройки.
- Настраиваем Snapfile.
- Указываем места, где хотели бы получить скриншот.
Snapfile
Мы можем указать вообще все девайсы, на которых будут прогоняться UI-тесты, скриншоты будут сделаны в одних и тех же местах на разных девайсах.
devices ([
"iPhone 6s"
])
scheme: "our scheme"
output_directory: "./path/."
stop_after_first_error: Bool
reinstall_app: Bool
clear_previous_screenshots: Bool
Erase_simulator: Bool
languages ([
"en-US"
])
Кроме того, указываем схему, на которой прогоняются UI-тесты, папку, где будет храниться результат всех скриншотов, и вспомогательные настройки.
Также мы можем указать языки, на которых будем прогонять наши UI-тесты. Если у вас много локализаций, вы можете тут указать все свои локализации и на выходе получить скриншоты с разными языками.
Time for screenshot
С технической точки зрения, в самом коде нужно просто добавить определенный файл — SnaphotHelper.swift в таргет с тестами. Этот файл генерируется в вашей папке с проектом, когда вы делаете fastlane snapshot init, его просто нужно переложить в таргет.
Далее взываем внутри метода setup(): [Snapshot setupSnapshot:app] внутри всех тест-кейсов, где хотим сделать скриншоты, и передать туда XCUI Application.
Последнее, что остается, просто в коде пробежаться по местам, где мы хотим сделать скриншот, указать имя скриншота, с которым сгенерируется картинка [Snapshot snapshot:@"Name screen"waitForLoadingIndicator:YES], запустить из терминала, сделать fastlane snapshot и радоваться.
После прохождения тестов там будут не только картинки разных экранов, но сгенерируется HTML страничка.
На ней все картинки разбиты по категориям, например, по девайсам и по локализации, которые вы указали в Snapfile. Правда, единственный минус — все эти скриншоты сортируются не по последовательности их выполнения, а по названию.
Обрати внимание!
Перед тем, как вы решите внедрять UI-тесты, стоит обратить внимание на несколько моментов:
- Не надо бояться Stub manager — его 100% надо реализовывать. На реальной сети тестировать плохо.
- Page object pattern — надо бороться за чистоту своего кода, даже если он написан для UI-тестов.
- Вы можете легко внедрить инструмент Snapshot. Если у вас готовы все тест-кейсы, написан весь код, это делается максимум в течение полудня.
В нашем проекте мы получили следующие преимущества:
- Уменьшили количество багов с дизайном почти до нуля.
- Внедрили инструмент для быстрой генерации всех скриншотов приложения и интегрировали тесты с CI.
- Сделали проверку правильного роутинга в приложении.
Я про это не говорил, но у нас довольно сложный роутинг. В разных странах он может отличаться. Мы просто на каждом новом экране проверяем специфичный элемент, который есть только на этом экране, и, если его нет, это значит, что мы находимся не в том месте, что-то пошло не так. Так мы проверяем роутинг.
Но мы также получили и небольшие минусы:
- Длительность внедрения UI-тестов. Создать базовую основу: реализовать Page object pattern, внедрить Snapshot, Stub manager — делается очень быстро, но время тратится на продумывание тест-кейсов и отладку.
- Ваши тесты также надо будет поддерживать при рефакторинге. В нашем случае это делается довольно часто. Конечно, на это тратится не так много времени, но про это не надо забывать.
Как и обещал, в конце приведу свои мысли, когда же реально стоит внедрять UI-тесты.
Использовать тесты стоит:
- Если у вас долгосрочный проект — UI-тесты 100% надо внедрять. Если вы постоянно развиваете свой продукт, вы должны отвечать за качество дизайна в том числе.
- Чтобы вести диалог по поводу Pixel Perfect и оперировать не словами «там что-то поехало», а выдавать скриншоты всего вашего приложения, где будет видно, что надо поправить.
- Если вы имеете сложную логику навигации в приложении, вам тоже стоит внедрить UI-тесты, это очень сильно помогает. Вы можете даже на дизайн особого внимания не обращать, но реализовать логику для проверки навигации довольно просто.
НовостиВ этом году мы решили вынести конференцию по мобильной разработке в отдельное мероприятие — теперь AppsConf Moscow будет полностью обособленным мероприятием и пройдет 8 и 9 октября. Мы планируем провести самое большое мероприятие по мобильной разработке в России, собрать активистов всех сообществ мобильных разработчиков со всей страны, представить более 60 докладов для более чем 500 человек.
Но и на фестивале РИТ++ будет много разносторонних смежных докладов, например, мы получили следующие заявки:
Mobile DevOps: автоматизируем и улучшаем процесс мобильной разработки / Вячеслав Черников (Binwell)
Микросервисы на фронтенде в почте Mail.ru / Егор Утробин (Mail.ru)
Интеграционное тестирование микросервисов на Scala / Юрий Бадальянц (2ГИС)
Изучайте список заявок, с которым, правда, в течение месяца еще будет работать ПК, и, если заинтересовались, бронируйте билеты.
Комментариев нет:
Отправить комментарий