Прокачиваем WebDriverAgent, или как тестировать iOS-приложения после ядерного взрыва. Расшифровка доклада
Когда Apple с выходом Xcode 8 отказались от UI Automator, мы, как и многие, оказались у разбитого корыта. Appium, который у нас использовался, потерял актуальность, мы начали искать альтернативы и нашли инструмент WebDriverAgent от Facebook. Под катом — текстовая расшифровка доклада о том, с какими проблемами мы столкнулись, как мы их решали и как это повлияло на нашу инфраструктуру тестирования iOS-приложений.
Avito — это несколько веб-интерфейсов, API и приложения под iOS и под Android. Всё это надо тестировать, поэтому у нас есть свой тестовый фреймворк. Он выглядит примерно так:
И состоит из двух основных частей.
Первая — это, конечно, сами тесты. Они представляют собой набор высокоуровневых шагов: залогиниться, открыть страничку, разлогиниться и так далее. Работают они со стейджами, где описана работа со всеми конкретными элементами: нажать на кнопку, заполнить поле ввода и так далее. И (куда же без них?) под этим всем скрываются page objects, в которых описаны сами элементы, их локаторы, описания.
Вторая часть фреймворка — большой набор библиотек, которые позволяют создать объявление, найти готовое, удалить его, что-то сделать с пользователем, еще что-то, применить какие-то услуги и так далее. Короче, все, что нужно для получения предсостояния теста.
Все тесты «общаются» с тестируемыми приложениями, кроме API, через WebDriver протокол. Для iOS мы использовали Appium. Всё было круто: у нас было тестовое покрытие, всё работало. А потом Apple объявила о выходе Xcode 8. Ключевая его особенность в том, что они полностью отказались от UI Automation, который был в инструментах Apple. И вся эта схема просто перестала работать.
Надо было принимать какое-то решение. Apple предлагает взамен писать тесты на Swift. Но мы не хотим: у нас большой набор библиотек, 200 тысяч строк кода, 400 тестов под разные приложения и так далее. Не хочется всё это терять. В качестве замены мы нашли инструмент от Facebook — WebDriverAgent. Система его работы похожа на Appium, он тоже поднимает веб-сервер, правда, сразу на девайсе или на симуляторе, и транслирует вызовы из тестовых скриптов через XCUITest на тестируемое приложение.
Плюсы WDA
Что умеет WebDriverAgent? Поддерживает Json Wire протокол. Это значит, что мы сохраняем свои тесты и весь тестовый фреймворк. Под капотом у него XCUITests. Это круто, потому что это технология, которую поддерживает Apple, и есть шанс, что еще года два-три она продержится. Написан WebDriverAgent на ObjC, благодаря этому нам не нужно переписывать всё с каждым новым релизом Xcode.
Также из плюсов: WDA поддерживает различные стратегии локаторов, можно искать по типу элемента, по имени, XPath — все как мы любим. Из дополнительных плюсов — позволяет работать с системой вне приложения. Мы можем сходить в настройки, в Safari, вбить DeepLink, открыть сразу приложение, где нам надо и так далее. В настройках запретить или разрешить геолокацию и так далее. Ещё WDA поддерживает технологию Touch ID, это отдельный плюс этого инструмента.
Минусы WDA
Есть и минусы. Первый — Инспектор. В любой системе функционального тестирования он занимает очень важную роль, это то, как автотестер видит приложение, код страницы на экране и так далее. У WebDriverAgent с этим довольно плохо. Конкретнее расскажу об этом ниже.
И ключевая проблема — в нашей инфраструктуре он работает медленно. Причем настолько, что им нельзя пользоваться. Но это всё мы узнали не сразу. Сначала было всё круто, но не заработало примерно у половины наших автотестеров.
Hacking
Первая правка, которую мы внесли в WebDriverAgent была вот такой.
Ребята из Facebook не запаривались: ведь все знают, что в MacOS — регистронезависимая файловая система. Но у нас в отделе мы пишем код, который выполняется преимущественно на Linux-серверах, поэтому мы сразу советуем всем переставить MacOS, и кто так сделал, не смогли скомпилировать WDA: просто перепутали букву.
Следующая смешная правка, которую мы внесли, выглядела так.
Это — метод стирания текста из поля. Правка ускорила некоторые наши тесты примерно на 30 секунд. Почему? Основная проблема в том, что длина итерируемого массива вычислялась прямо в теле цикла. Обычно в компилируемых языках вам не нужно об этом думать: там есть оптимизатор, он делает всё за вас. Но в системах функционального тестирования это не работает. Потому что у нас есть элемент на экране, который лежит где-то в кэше WDA, и который надо достать оттуда, найти на странице, взять его атрибуты, найти среди них value и стереть один символ. Потом снова пойти, снова достать элемент из кэша, снова найти его на странице, снова вычислить длину поля value и снова стереть один символ. У нас поле описания, по-моему, 1000 символов. Минус 30 секунд.
Но и это не самое страшное. Самое страшное выглядело примерно так:
Здесь — страница «Уточнить» в поиске в iOS-приложении Avito. Он осуществляется через XPath. Ищутся два элемента: минимальная цена и максимальная. Это видео ускорено в 6,5 раз. Реальное время прохождения — минута сорок. Из них 20 секунд я вожу руками по экрану, набиваю текст и так далее. По 40 секунд выполняется каждый из двух запросов на поиск элементов. При этом «сжираются» лишние 16 Мб оперативной памяти. Мы подумали и поняли, что жить с этим нельзя: сейчас это занимает 40 секунд, а если пройдет половина теста, наберется еще памяти, запросы начнут выполняться еще дольше. И либо WebDriverAgent упадет, когда использует слишком много памяти, либо у нас HTTP-запросы будут отваливаться по таймауту.
Мы посидели, посмотрели, что мы можем с этим сделать, и нашли решение: придумали свою систему аллокаторов. Назвали её XUI: eXtended UI Interator. Это просто хорошо отражало наше отношение к этому на тот момент. Под её капотом — биндинги на XCUI локаторы. Вот второе демо:
Ищутся ровно те же элементы, но теперь через наши новые локаторы. Видео ускорено в два раза, реальное время его прохождения — 20 секунд, и это те же 20 секунд, что я вожу руками по экрану, потому что каждый запрос выполняется меньше, чем за одну секунду. И используется всего 1,5 Мб памяти.
Как мы этого добились? Если кто-то писал тесты на XCUI нативно, то он знает, что там всё начинается с того, что у нас есть объект application, из которого мы потом ищем просто элементы, например, текст, кнопка с надписью on и так далее. Если что-то посложнее — можно найти элемент по индексу и так далее.
XCUI селекторы:
- let app = XCUIApplication ()
- app.staticTexts[«Volley»]
- app.buttons[«On»]
- app.windows.element (boundBy: 0)
В реальной жизни, правда, это скорее вот так выглядит:
- app.children (matching: .window).element (boundBy: 0).children (matching: .other).element.children (matching: .other).element.children (matching: .other).element.children (matching: .other).element.children (matching: .other).element.children (matching: .other).element.children (matching: .other).element.children (matching: .other).element (boundBy: 0).children (matching: .other).element (boundBy: 1).children (matching: .other).element.children (matching: .other).element (boundBy: 0).children (matching: .other).element.children (matching: .button).element.tap ()
Но суть остается такой же: есть какой-то parent, у него есть либо прямой потомок из какого-то типа, либо, если вглубь по дереву, то непрямой.
Поэтому нам нужно знать тип элемента и иметь какой-то признак прямого и непрямого потомка. Мы залезли в WebDriverAgent, начали кодить. Взяли тип элемента. Если нам тип не важен — просто *. Точка показывает, прямой или непрямой потомок, children ищем или descendants.
Всё круто, но это всё может быть вложено, поэтому нам нужен какой-то разделитель. Мы выбрали pipe (»|»), просто чтобы не путаться с XPath.
И всё, что было выше, выполняется теперь в цикле.
С этим разобрались. Дальше нужен выбор по индексу элемента. Если пришел индекс, если мы задетектили, что он нам нужен, выбираем просто из коллекции найденных элементов. Здесь есть ключевое слово last, чтобы брать сразу последний.
Самое важное — нам еще нужен выбор по сложным условиям, потому что у XPath есть XPath access и куча других функций. И тут нас сильно выручил NSPredicate, класс, который поставляется в Foundation Framework Apple и служит для фильтрации и выборки элементов из коллекции.
По сути, он умеет очень много. Тут ссылка, можно почитать.
Кто помнит, в Appium во времена UI Automation были BEGINSWITH, MATCH и так далее, это прямо оно. Синтаксис — что-то среднее между RegExr и секцией WHERE в SQL.
Мы это скомпоновали и получились такие локаторы. В круглых скобках — NS предикаты, в квадратных — индексы, пайпы и точки.
XUI-локаторы
- xui=StaticText[1]|TextView[0]
- xui=Button (label == 'Stop')
- xui=NavigationBar[last]
- xui=Table[0]|Cell[3]|.StaticText (id=Address)
- xui=Table[0]|Cell[3]|.StaticText (id=Time)
С этим разобрались: заработало с приемлемой скоростью, начали разбираться со следующей проблемой. Инспектор. У WebDriverAgent инспектор есть, есть даже классная инструкция, как его запустить, можно выполнить раз команду, два, три и… Не завелось, короче:
Даже если бы завелось, там тоже ничего полезного. Поэтому нам пришлось написать свой. Он тоже простенький, но довольно функциональный.
Есть дерево элементов, зеленым отмечены accessibilityID, если они проставлены. Можно выбрать элемент и в правом верхнем углу можно увидеть информацию по нему, в левом — на скриншоте посмотреть, где конкретно он расположен на экране. Ключевое, что было нам нужно — строка поиска, чтобы тестировать, правильно мы составили локаторы или нет, ищется по нему элемент на странице или нет.
Итоги
Что в итоге? Был WebDriver со своими плюсами и минусами. Мы над ними немного поработали —
стало сильно лучше.
На нашей схеме Appium поменялся на WebDriverAgent:
Тесты мы оставили как есть:
Стейджи оставили как есть. В page objects поменяли локаторы Appium на наши. Они стали даже более читаемыми.
На всё ушло примерно 2–3 недели и сэкономило нам 200 000 строк в библиотеках и примерно 400 тестов.
На этом мы не остановились, конечно.
Appium помимо того, что просто позволял тестировать, решал еще какие-то задачи. Теперь нам пришлось заниматься ими самим. Например: мы хотим параллелить тесты, нам нужно запустить несколько инстансов WebDriverAgent, запустить тесты, в каждом указать API и так далее.
Но ведь люди придумали grid! Однако вот проблема: когда подключаешь больше трех нод, ему сносит башню, он начинает жрать память, течь, тупить. Некоторые люди извращаются примерно вот так. Не пытайтесь здесь ничего понять:
Здесь есть grid, за ним еще 4, за ними еще 4 и так далее. Я посчитал: с учетом особенностей нашего проекта нам бы пришлось бы сто таких гридов ставить, целый сервер на это выделять. Мы не стали этим заморачиваться. Мы написали свой. Он простой, написан на Go, использует примерно 10 Мб оперативной памяти и обслуживает 300 нод. Регистрирует ноды, проксирует все вызовы на эти ноды и выбирает подходящую с учетом capabilities на запрос сессии. И в конце надо ее освободить. Либо когда сессию закрыли, либо по тайм-ауту, если тест свалился и не смог сообщить, что закончил. Всё это совместимо с Selenium grid, чтобы можно было работать. Теперь у нас есть вот эта схема, она рабочая на этот раз:
Но проблемы наши на этом не заканчиваются, потому что мы тестируем приложение, надо его на телефон как-то поставить, что-то с ним сделать, поэтому мы написали штуку под названием grid-wda-agent:
Она делает несколько простых вещей: регистрируется в grid, потому что сам WebDriverAgent этого не умеет. Проксирует все вызовы на WDA, и на старт сессии выбирает или запускает нужный симулятор, который мы запросили в Capabilities, удаляет старую версию приложения, ставит новую, и перезапускает WDA, если надо, потому что он все равно иногда поджирает память, его лучше иногда рестартовать. И дополнительно он записывает видео прохождения теста и отправляет его потом в хранилище по S3 протоколу.
Всё это мы положили на Github в организацию qa-dev, можно заходить, читать, слать pull requests, issues.
Выводы
Что теперь делать, если в 2018 году мы хотим тестировать iOS-приложения?
Два пути. Если умеем Swift/ObjC — мы можем писать нативные тесты, XCUI Tests, ждать, пока это будет компилироваться, чтобы проверить один тест. Или можем взять WebDriverAgent. Тут выбор побольше: есть либо Appium, разработчики которого спустя полгода спустя выхода XCode 8 все-таки запилили свою реализацию поверх WDA Agent, форкнув его и что-то добавив. Есть оригинальная версия от Facebook. И наша.
Что есть у Facebook? Он официальный, в него все потихоньку контрибутят.
Есть Appium. Во-первых, процитирую Дэна, который Appium разрабатывает.
«De-facto standard for automating mobile applications».
Во-вторых, у Appium есть поддержка open-source сообщества. И ребята недавно запилили свой инспектор в виде приложения для MacOS.
Есть наш вариант. У нас есть быстрые локаторы, свой инспектор, Grid, Grid-агент. Последние три, в принципе, работают с любым WebDriverAgent, можно их брать отдельно и пробовать использовать.
Какие выводы мы можем сделать?
Первое — выбирайте то, что подходит вам. Второе — не бойтесь делать свои инструменты. В 2015 году моя жизнь выглядела примерно так:
Потом в 2016 году начали происходить все эти события, мы начали писать свою «костылевую» версию WDA. Стало так:
Сейчас у нас есть Grid, Grid-агент… Жизнь-то налаживается!
Когда делаете свои велосипеды, делайте то, что нужно вам для проекта. И более важно — не делайте то, что не нужно. Потому что когда вся история происходила, мы думали: «классно, есть XPath, мы сейчас его возьмем и сделаем нормально, а не как у них, и будет работать». Но XPath имеет настолько мощный синтаксис и настолько много функций, что мы, наверное, бы до сих пор писали, если бы пошли этим путем. Нам нужны были быстрые локаторы, а не сделать XPath.
Пожалуй, на этом всё. Задавайте свои вопросы.