Привет! Давайте поговорим о том, как сейчас в 2020-ом году можно протестировать мультиязычное iOS приложение, если не хочется проверять локализацию вручную.
Мы — финтех-компания, и наша прибыль напрямую зависит от объема торгов. Чем больше наши клиенты открывают ордеров (или сделок), тем больше прибыли получаем мы. А количество сделок напрямую зависит от депозитов: чем больше депозитов, тем больше ордеров может открыть клиент и, соответственно, на больший объем. Но в разных странах удельный размер депозитов сильно различается. Например, в Таиланде в четыре раза больше пользователей, чем во Вьетнаме, но по депозитам этого не скажешь.
И некоторое время назад наш Product Owner подумал о причинах. Оказалось, что на это влияет отсутствие локализации — интерфейс приложения на тайском языке, а названия местных тайских банков отображались на английском.
Мы провели эксперимент: перевели названия, и после релиза количество депозитов возросло в разы.
То есть перевод 6-7 строчек текста на нужный язык в нужном месте может принести очень большую выгоду.
Наше приложение
Немного расскажу о нашем приложении Exness Mobile Trader.
Это личный кабинет трейдера, в котором мы сделали свой собственный торговый терминал на WebSocket. Приложение умеет работать с большим количеством международных и региональных платёжных систем. Есть много преднастроенных сервисов, которые позволяют пользователю торговать эффективнее. Также приложение поддерживает несколько типов счетов: реальные счета с настоящими деньгами, демо-счета для тренировки и крипту. Вишенка на нашем торте — это гибкая система push-уведомлений.
Всё это есть в приложении сейчас, но так было не всегда. История началась около двух лет назад.
В начале 2017 года мы поняли, что 70% пользователей нашей торговой системы заходят в свой личный веб-кабинет через мобильные браузеры, то есть через телефоны и планшеты. И мы решили создать свое мобильное приложение. Сделали пробную версию, но она оказалась неудачной. А в сентябре 2017-го начали работать над основным приложением. Работа заняла год. Мы экспериментировали. Например, в тестировании пробовали BDD-подход, автоматизировали API в Postman. К сентябрю 2018 года был запланирован релиз, и где-то за пару месяцев до этого встал вопрос локализации, так как у нас очень много клиентов по всему миру.
Локализация
Локализация — это процесс адаптации и интернационализации под конкретный регион. Добавление специализированных компонентов, характерных для определенной локали, и перевод текста.
Первая проблема — как оперативно перевести мобильное приложение на несколько языков за короткий срок, когда у тебя разработчики сидят на Кипре, а переводчики в Азии за пять часовых поясов?
Нашим решением стал Crowdin — система управления мультиязычным контентом. Это огромный комбайн, в котором как в Jira, заводишь задачи и распределяешь по всевозможным исполнителям: переводчикам, менеджерам, тестировщикам. Можно голосовать за понравившийся перевод, оставлять комментарии. Благодаря этому инструменту мы довольно быстро перевели приложение на разные языки.
Crowdin всеядный: ему можно скормить XML, так YML, JSON, строки. Он всё распарсит и будет с этим работать. Он не позволяет переводчикам что-то поломать: мухи строки отдельно, код отдельно. У Crowdin крутой API. Можно чуть ли на каждый коммит повесить создание новой задачи на перевод. Инструмент очень гибкий, позволяет работать как с целым файлом для какого-то языка под определенную фичу, так и с отдельными строками. Можно быстро посмотреть, как эта строчка переведена на родственные языки, это актуально, например, для китайского.
А недостаток Crowdin в его довольно высокой стоимости.
После перевода возникла новая проблема: как всё это быстро загрузить и проверить?
В этом помог LinguanApp — «умный» редактор строк в Xcode-проекте, отображающий их в более-менее удобоваримом виде. Он позволяет посмотреть, как выглядят строчки на разных языках, и тут же что-то подправить. Реализована обратная совместимость: сделанные изменения отображаются во взаимосвязанных локациях. В LinguanApp есть удобная функция автоматической валидации: после загрузки всех данных система проверяет, где и что не подгрузилось. Благодаря знакомому, экселеподобному интерфейсу, этот инструмент отлично подходит для управления процессом локализации.
Недостатки: LinguanApp тоже стоит денег, но не так много, как Crowdin.
Тестирование
Итак, переводы загружены. Нужно проверить, как это выглядит на устройствах, протестировать локализацию — убедиться, что приложение ведет себя так же, как до локализации. В нашем случае тестирование локализации заключается в проверке, что все строки одной локали переведены. Не менее важно, чтобы все слова и фразы помещались в отведенные места в интерфейсе, ничто не накладывалось друг на друга и не обрезалось:
Какие сложности могут возникнуть при ручном тестировании локализации? Чтобы пользователю было удобно пользоваться вашим приложением, нужно проверить, как оно выглядит при всех поддерживаемых вами разрешениях экранов и на всех языках, которых может быть очень много: наше приложение кроме английского переведено еще на 14 языков. У одного только iPhone сейчас шесть размеров экранов (если говорить о телефонах, начиная от iPhone SE), итого 6 х 15 = 90 комбинаций «язык — разрешение». Вручную это проверить практически нереально. Хотя изначально мы выпустили приложение только на двух языках, так что протестировать его вручную ещё можно было. Но даже тогда у нас возникли трудности.
Во-первых, у нас не было всех видов устройств: на момент релиза приложения ещё отсутствовал в продаже iPhone Xs Max, а в наличии у нас были только iPhone X, 6S и 6 plus. Конечно, можно было пользоваться симулятором, но это не очень хороший вариант. Мы решили воспользоваться функцией Display Zoom:
В чем суть? Вы можете задать у себя разрешение другого смартфона, картинка и текст станут крупнее. Эта функция появилась в iPhone 6S, который в режиме Display Zoom переходил в разрешение iPhone 5. В iOS 11 и iPhone Xs нет Display Zoom, а в Xr и Xs Max он работает одинаково.
Нам не понравилось тестировать вручную. Неплохо было бы это автоматизировать. Точнее, автоматически создавать скриншоты.
Автоматическое создание скриншотов
Сначала необходимо подготовить тесты. Проблема в том, что универсального решения не существует. То есть для автоматического создания скриншота вам нужен UI-тест. Однако традиционные UI-тесты, которые завязаны на элементы интерфейса, при смене локалей будут падать. Чтобы этого не происходило, нужно добавить элементам интерфейса accessebility Identifier’ы:
signinButton.accessebilityIdentifier = "btn_auth"
Сделать это можно как в коде, так и в Identity Inspector в Xcode.
Для демонстрации инструментов, о которых пойдет речь дальше, я подготовил простой тест. Он написан на Swift с помощью XCTest.
func testTutorial() {
tutorialButton.tap()
waitForElementToDissappear(element: tutorialButton, timeout: 5)
makeScreenshot()
for _ in 1...4 {
app.swipeLeft()
makeScreenshot()
}
tutorialCloseButton.tap()
waitForElementToDissappear(element: tutorialCloseButton, timeout: 3)
}
Запускается приложение, нажимаем кнопку Tutorial. После её исчезновения делаем скриншот. Потом в цикле смахиваем влево, каждый раз делая скриншот. Закрываем tutorial и ждем, чтобы кнопка исчезла.
Вот как это выглядит на симуляторе:
Это скопированный со Stackoverflow метод создания скриншотов:
func makeScreenshot() {
XCTContext.runActivity(named: "Making a full screenshot and saving it") { (activity) in
let screen = XCUIScreen.main
let fullscreenshot = screen.screenshot()
let fullScreenshotAttachment = XCTAttachment(screenshot: fullscreenshot)
fullScreenshotAttachment.lifetime = .keepAlways
activity.add(fullScreenshotAttachment)
}
}
Это метод ожидания исчезновения элемента:
func waitForElementToDissappear(element: XCUIElement, timeout: Double) {
let doesNotExistPredicate = NSPredicate(format: "exists == FALSE")
expectation(for: doesNotExistPredicate, evaluatedWith: element, handler: nil)
waitForExpectations(timeout: timeout, handler: nil)
}
Есть два способа автоматического создания скриншотов на основе теста.
Первый — с помощью Fastlane, здоровенного комбайна для автоматизации рутинных задач. Он умеет делать очень много чего, включая конфигурирование и запуск тестов.
Настроить его не сложно:
По итогу создадутся два файла SnapshotHelper.swift и Snapfile, мы к ним еще вернемся.
Потом нужно будет для Fastlane создать UI Test Target. Берем UI Test Bundle:
Указываем цель для тестирования:
Потом обязательно указываем Target membership и переносим туда созданные файлы.
Теперь мы создаем для целевого объекта новую схему. Убеждаемся, что у неё свойство shared. Удостоверяемся, что секция Build выглядит примерно так:
а Test вот так:
Теперь нам нужно инициализировать Fastlane с помощью функции setUp(), которая исполняется перед каждым запуском вашего тест-класса:
override func setUp() {
continueAfterFailure = false
setupnapshot(app)
app.launch()
}
И нужно будет немного поменять тест, который мы недавно написали, чтобы инициализировать вызов метода создания скриншотов в Fastlane. А именно заменить функцию makeScreenshot() на snapshot("snapshot_name"), то есть Fastlane позволяет заранее настраивать названия скриншотов.
Плюс надо немного переписать цикл, чтобы у каждого скриншота было уникальное имя. Вот что у нас получилось:
func testTutorial() {
tutorialButton.tap()
waitForElementToDissappear(element: tutorialButton, timeout: 5)
snapshot("Tutorial_page_1")
var i: Int = 2
repeat {
app.swipeLeft()
snapshot("Tutorial_page_\(i)")
i += 1
} while i <= 5
tutorialCloseButton.tap()
waitForElementToDissappear(element: tutorialCloseButton, timeout: 3)
}
Для демонстрации давайте ограничимся 4 размерами экранов и 5 языками: русским, китайским, тайским, вьетнамским и корейским. Чтобы задать эти параметры, нам нужно настроить снэпшот файл — по сути своей, конфиг. Он выглядит так:
В list of device мы перечисляем устройства, для которых будем делать скриншоты; также указываем языки и схему, которая содержит UI-тесты, которые мы создали. Затем указываем место, куда будут сохраняться скриншоты, и задаём порядок действий с предыдущими скриншотами — нужно ли их удалять.
После запуска командой fastlane snapshot мы получаем HTML-страницу со скриншотами, которые можно группировать как по языку, так и по типу экрана.
Однако недостатком Fastlane является низкая скорость работы. Даже для нашего маленького проекта из 5 страниц программа генерировала скриншоты под 4 разрешения и на 5 языках целых 28 минут. А на реальном проекте уходили часы. Поэтому мы отказались от Fastlane.
Ещё один инструмент для автоматического создания скриншотов — XCTest Plan. Его представили на WWDC 2019 вместе с Xcode 11. Новая функциональность позволяет тестировщикам и разработчикам конфигурировать тесты согласно своим потребностям: определять, какие тесты запускать в сборке и в каком порядке, что делать с артефактами. И самое главное, XCTest Plan позволяет нам относительно безболезненно и быстро создавать скриншоты прямо внутри Xcode без внешних зависимостей.
Давайте пробежимся по настройке. Прежде всего, нужно создать схему для последующей конвертации, чтобы можно было использовать XCTest Plan:
После конвертации получим конфигурационный файл:
И теперь нужно сделать по копии для каждого из выбранных языков. Эти копии будут отличаться лишь строкой application language:
В Shared settings мы оставляем system language, это, в нашем случае, английский язык:
Теперь остается только запустить тест. Это можно сделать двумя способами:
правой кнопкой —> Run yourTestName():
причем можно запустить тест как во всех конфигурациях сразу, так и отдельно для каждого языка
и командой Xcodebuild:
Xcodebuild
-workspace ExnessForHeisenbug.xcworkspace/
-scheme ExnessForHeisenbug
-destination 'platform=iOS Simulator,OS=10.3.1,name=iPhone 6s'
-destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7 plus'
-destination 'platform=iOS Simulator,OS=12.2,name=iPhone Xs'
-destination 'platform=iOS Simulator,OS=12.2,name=iPhone SE'
test -testPlan ExnessForHeisenbug
Здесь мы указываем рабочее пространство, схему, testPlan и destination. Можно не только выбрать разные модели телефонов, но и задать им разные версии iOS, таким образом решив проблему фрагментации операционных систем.
Давайте запустим наш XCTest Plan. Он для четырёх симуляторов поочерёдно меняет локали и делает скриншот каждой страницы, а в конце удаляет симуляторы. Работает намного быстрее Fastlane.
На выходе мы получаем отчёт testTutorial:
Здесь проявляется один из недостатков XCTest Plan: смотреть скриншоты довольно неудобно. В Fastlane создаётся HTML-страница, в которой можно группировать скриншоты, а здесь каждый раз приходится нажимать на предпросмотр. Насколько мне известно, Xcode позволяет экспортировать изображения, но по какой-то причине мне это сделать не удалось. Либо мой XCTest Plan не так настроен, либо это баг Xcode.
В целом же это очень крутой нативный инструмент. Я надеюсь, что Apple будет его в дальнейшем поддерживать, развивать, править возникающие баги. То, что Fastlane сделал за 28 минут, XCTest Plan сделал за 2,6 минуты. То есть в 10 раз быстрее.
Анализ скриншотов
Мы получили скриншоты, теперь нужно их сравнить. Это можно сделать автоматически, так называемое снэпшот-тестирование: при первом запуске теста создаётся некое эталонное изображение, с которым будут сравниваться скриншоты при всех последующих запусках.
Среди бесплатных инструментов для снэпшот-тестирования хочу отметить два фреймворка:
Первый — это iOS Snapshot Test Case, библиотека, написанная на Objective C. Она преобразует UIView/CALayer в изображения и сравнивает их. При первом запуске записываем (recordMode = true) референс (эталон). При последующих запусках (recordMode = false), сравниваем полученные скриншоты с эталоном.
Здесь на второй картинке сместились логотипы и фраза Deposit is in your account! Long title! Test it Elon Musk!, а также изменился цвет надписи 120 000 000.00 USD. То есть сразу видно, в чем проблема.
Этот фреймворк довольно гибко настраивается. Мы можем поставить заплатки вместо динамических элементов, из-за изменения которых тесты падают. Например, можно сделать заплатку вместо transaction id, который уникальный в каждом тесте.
Обратите внимание на отсутствие Статус бара (панели с часами). Мы её отрезали, потому что время меняется, и тест падает.
Для настройки фреймворка нужно указать pod
и прописать две переменные окружения:
FB_REFERENCE_IMAGE_DIR — куда кладётся эталон, и
IMAGE_DIF_DIR — куда будут складываться дифы при сбое теста.
Одним из достоинств фреймворка является визуализация различий между скриншотами. Также он по-человечески именует скриншоты, красиво раскладывает их по папкам. Почему это важно, вы поймете, когда я расскажу о следующем фреймворке.
Второй фреймворк — это Swift Snapshot Testing, он создан в Point Free Co. Работает по тому же принципу: записывает эталон и сравнивает с ним. Фреймворк принимает и JSON, и дампы, и URL, практически что угодно.
Настраивается он тоже довольно просто. Eсли импортировать модуль Import Snapshot Testing, то в функции assertSnapshot() мы будем сравнивать ViewController как изображение. При первом запуске фреймворк создаст эталон и будет сообщает о несовпадениях с ним при всех последующих запусках.
import SnapshotTesting
import XCTest
class MyViewControllerTests: XCTestCase {
func testMyViewController() {
let vc = MyViewController()
assertSnapshot(matching: vc, as: .image)
}
}
К достоинствам инструмента можно отнести то, что он написан на Swift и всеяден. Главный недостаток — отсутствие diff: фреймворк лишь сообщает о самом факте несовпадения. плюс бардак с наименованием и размещением скриншотов. Допилить его можно, благо, что он open source, но из коробки работает не так, как бы нам хотелось.
Резюме
Мы поговорили о двух инструментах, которые упрощают выгрузку и проверку локализованных текстов для приложений. Затем рассмотрели два фреймворка — новый XCTest Plan и старый Fastlane. С их помощью можно автоматически создавать скриншоты интерфейса. И в заключение рассмотрели два инструмента для снэпшот-тестирования.
У нас в Exness сейчас два мобильных проекта: Exness Trading, личный кабинет трейдера, и Social Trading, позволяющий просто положить деньги и копировать сделки других более опытных трейдеров.
Crowdin у нас до сих пор используется в обоих проектах. От LinguanApp в Exness Trading мы отказались, потому что у нас довольно много плейсхолдеров, которые актуальны только для одной локали, а автоматическая валидация постоянно давала сбои. Зато этот инструмент продолжает использовать команда Social Trading. Также в этом проекте используется Fastlane для создания и загрузки билдов. С XCTest Plan мы экспериментируем в обоих приложениях. Наконец, в Exness Trading мы внедряем Swift Snapshot Test Case, а в Social Trading — iOS Snapshot Test Case. Спасибо что дочитали до конца, надеюсь эта статья будет вам полезной. Happy testing!
Что еще можно почитать/посмотреть по теме:
Fastlane:
https://agostini.tech/2018/07/15/automatic-screenshots-with-fastlane-snapshot/
https://docs.fastlane.tools/getting-started/ios/screenshots/
XCTestPlan:
https://shashikantjagtap.net/wwdc19-getting-started-with-test-plan-for-xctest/
https://developer.apple.com/videos/play/wwdc2019/403
https://developer.apple.com/videos/play/wwdc2019/413
swift-snapshot-testing:
https://github.com/pointfreeco/swift-snapshot-testing/
ios-snapshot-test-case:
https://github.com/uber/ios-snapshot-test-case/
Display Zoom:
http://www.iphonehacks.com/2014/09/use-display-zoom-iphone-6-plus.html
freiman
Это, конечно, все хорошо… но невлезающие переводы — это небольшая часть проблем, которая довольно легко обнаруживается.
А вот что хуже — это проверка качества перевода. Это как-то контролируется?
Я имею в виду ошибки перевода вроде
На английском оно, может, было понятно и слова там разные Discard/Cancel/Undo, а при переводе получилось то, что получилось…
smokhin Автор
У нас это зона ответственности менеджеров по локализации. Команда разработки пока владеет только английским, русским и украинским языками. Если мы видим подобные ошибки на языках, которые мы знаем, то правим. Ошибки перевода на тайский, китайский и прочие языки, мы, к сожалению, со своей стороны проверить не можем