Современный подход к тестированию локализации на iOS

от автора

Привет! Давайте поговорим о том, как сейчас в 2020-ом году можно протестировать мультиязычное iOS приложение, если не хочется проверять локализацию вручную.

image

Мы — финтех-компания, и наша прибыль напрямую зависит от объема торгов. Чем больше наши клиенты открывают ордеров (или сделок), тем больше прибыли получаем мы. А количество сделок напрямую зависит от депозитов: чем больше депозитов, тем больше ордеров может открыть клиент и, соответственно, на больший объем. Но в разных странах удельный размер депозитов сильно различается. Например, в Таиланде в четыре раза больше пользователей, чем во Вьетнаме, но по депозитам этого не скажешь.

image

И некоторое время назад наш Product Owner подумал о причинах. Оказалось, что на это влияет отсутствие локализации — интерфейс приложения на тайском языке, а названия местных тайских банков отображались на английском.

image

Мы провели эксперимент: перевели названия, и после релиза количество депозитов возросло в разы.

image

То есть перевод 6-7 строчек текста на нужный язык в нужном месте может принести очень большую выгоду.

Наше приложение

Немного расскажу о нашем приложении Exness Mobile Trader.

Это личный кабинет трейдера, в котором мы сделали свой собственный торговый терминал на WebSocket. Приложение умеет работать с большим количеством международных и региональных платёжных систем. Есть много преднастроенных сервисов, которые позволяют пользователю торговать эффективнее. Также приложение поддерживает несколько типов счетов: реальные счета с настоящими деньгами, демо-счета для тренировки и крипту. Вишенка на нашем торте — это гибкая система push-уведомлений.

image

Всё это есть в приложении сейчас, но так было не всегда. История началась около двух лет назад.

В начале 2017 года мы поняли, что 70% пользователей нашей торговой системы заходят в свой личный веб-кабинет через мобильные браузеры, то есть через телефоны и планшеты. И мы решили создать свое мобильное приложение. Сделали пробную версию, но она оказалась неудачной. А в сентябре 2017-го начали работать над основным приложением. Работа заняла год. Мы экспериментировали. Например, в тестировании пробовали BDD-подход, автоматизировали API в Postman. К сентябрю 2018 года был запланирован релиз, и где-то за пару месяцев до этого встал вопрос локализации, так как у нас очень много клиентов по всему миру.

Локализация

Локализация — это процесс адаптации и интернационализации под конкретный регион. Добавление специализированных компонентов, характерных для определенной локали, и перевод текста.

Первая проблема — как оперативно перевести мобильное приложение на несколько языков за короткий срок, когда у тебя разработчики сидят на Кипре, а переводчики в Азии за пять часовых поясов?

Нашим решением стал Crowdin — система управления мультиязычным контентом. Это огромный комбайн, в котором как в Jira, заводишь задачи и распределяешь по всевозможным исполнителям: переводчикам, менеджерам, тестировщикам. Можно голосовать за понравившийся перевод, оставлять комментарии. Благодаря этому инструменту мы довольно быстро перевели приложение на разные языки.

Crowdin всеядный: ему можно скормить XML, так YML, JSON, строки. Он всё распарсит и будет с этим работать. Он не позволяет переводчикам что-то поломать: мухи строки отдельно, код отдельно. У Crowdin крутой API. Можно чуть ли на каждый коммит повесить создание новой задачи на перевод. Инструмент очень гибкий, позволяет работать как с целым файлом для какого-то языка под определенную фичу, так и с отдельными строками. Можно быстро посмотреть, как эта строчка переведена на родственные языки, это актуально, например, для китайского.

А недостаток Crowdin в его довольно высокой стоимости.

После перевода возникла новая проблема: как всё это быстро загрузить и проверить?
В этом помог LinguanApp — «умный» редактор строк в Xcode-проекте, отображающий их в более-менее удобоваримом виде. Он позволяет посмотреть, как выглядят строчки на разных языках, и тут же что-то подправить. Реализована обратная совместимость: сделанные изменения отображаются во взаимосвязанных локациях. В LinguanApp есть удобная функция автоматической валидации: после загрузки всех данных система проверяет, где и что не подгрузилось. Благодаря знакомому, экселеподобному интерфейсу, этот инструмент отлично подходит для управления процессом локализации.

Недостатки: LinguanApp тоже стоит денег, но не так много, как Crowdin.

Тестирование

Итак, переводы загружены. Нужно проверить, как это выглядит на устройствах, протестировать локализацию — убедиться, что приложение ведет себя так же, как до локализации. В нашем случае тестирование локализации заключается в проверке, что все строки одной локали переведены. Не менее важно, чтобы все слова и фразы помещались в отведенные места в интерфейсе, ничто не накладывалось друг на друга и не обрезалось:

image

Какие сложности могут возникнуть при ручном тестировании локализации? Чтобы пользователю было удобно пользоваться вашим приложением, нужно проверить, как оно выглядит при всех поддерживаемых вами разрешениях экранов и на всех языках, которых может быть очень много: наше приложение кроме английского переведено еще на 14 языков. У одного только iPhone сейчас шесть размеров экранов (если говорить о телефонах, начиная от iPhone SE), итого 6 х 15 = 90 комбинаций «язык — разрешение». Вручную это проверить практически нереально. Хотя изначально мы выпустили приложение только на двух языках, так что протестировать его вручную ещё можно было. Но даже тогда у нас возникли трудности.

Во-первых, у нас не было всех видов устройств: на момент релиза приложения ещё отсутствовал в продаже iPhone Xs Max, а в наличии у нас были только iPhone X, 6S и 6 plus. Конечно, можно было пользоваться симулятором, но это не очень хороший вариант. Мы решили воспользоваться функцией Display Zoom:

image

В чем суть? Вы можете задать у себя разрешение другого смартфона, картинка и текст станут крупнее. Эта функция появилась в 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, здоровенного комбайна для автоматизации рутинных задач. Он умеет делать очень много чего, включая конфигурирование и запуск тестов.

Настроить его не сложно:

image

По итогу создадутся два файла SnapshotHelper.swift и Snapfile, мы к ним еще вернемся.
Потом нужно будет для Fastlane создать UI Test Target. Берем UI Test Bundle:

image

Указываем цель для тестирования:

image

Потом обязательно указываем Target membership и переносим туда созданные файлы.
Теперь мы создаем для целевого объекта новую схему. Убеждаемся, что у неё свойство shared. Удостоверяемся, что секция Build выглядит примерно так:

image

а Test вот так:

image

Теперь нам нужно инициализировать 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 языками: русским, китайским, тайским, вьетнамским и корейским. Чтобы задать эти параметры, нам нужно настроить снэпшот файл — по сути своей, конфиг. Он выглядит так:

image

В list of device мы перечисляем устройства, для которых будем делать скриншоты; также указываем языки и схему, которая содержит UI-тесты, которые мы создали. Затем указываем место, куда будут сохраняться скриншоты, и задаём порядок действий с предыдущими скриншотами — нужно ли их удалять.

После запуска командой fastlane snapshot мы получаем HTML-страницу со скриншотами, которые можно группировать как по языку, так и по типу экрана.

Однако недостатком Fastlane является низкая скорость работы. Даже для нашего маленького проекта из 5 страниц программа генерировала скриншоты под 4 разрешения и на 5 языках целых 28 минут. А на реальном проекте уходили часы. Поэтому мы отказались от Fastlane.

Ещё один инструмент для автоматического создания скриншотов — XCTest Plan. Его представили на WWDC 2019 вместе с Xcode 11. Новая функциональность позволяет тестировщикам и разработчикам конфигурировать тесты согласно своим потребностям: определять, какие тесты запускать в сборке и в каком порядке, что делать с артефактами. И самое главное, XCTest Plan позволяет нам относительно безболезненно и быстро создавать скриншоты прямо внутри Xcode без внешних зависимостей.

Давайте пробежимся по настройке. Прежде всего, нужно создать схему для последующей конвертации, чтобы можно было использовать XCTest Plan:

image

После конвертации получим конфигурационный файл:

image

И теперь нужно сделать по копии для каждого из выбранных языков. Эти копии будут отличаться лишь строкой application language:

image

В Shared settings мы оставляем system language, это, в нашем случае, английский язык:

image

Теперь остается только запустить тест. Это можно сделать двумя способами:

правой кнопкой —> Run yourTestName():

image

причем можно запустить тест как во всех конфигурациях сразу, так и отдельно для каждого языка

и командой 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:

image

Здесь проявляется один из недостатков 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), сравниваем полученные скриншоты с эталоном.

image

Здесь на второй картинке сместились логотипы и фраза Deposit is in your account! Long title! Test it Elon Musk!, а также изменился цвет надписи 120 000 000.00 USD. То есть сразу видно, в чем проблема.

Этот фреймворк довольно гибко настраивается. Мы можем поставить заплатки вместо динамических элементов, из-за изменения которых тесты падают. Например, можно сделать заплатку вместо transaction id, который уникальный в каждом тесте.

Обратите внимание на отсутствие Статус бара (панели с часами). Мы её отрезали, потому что время меняется, и тест падает.

Для настройки фреймворка нужно указать pod

image

и прописать две переменные окружения:

FB_REFERENCE_IMAGE_DIR — куда кладётся эталон, и
IMAGE_DIF_DIR — куда будут складываться дифы при сбое теста.

image

Одним из достоинств фреймворка является визуализация различий между скриншотами. Также он по-человечески именует скриншоты, красиво раскладывает их по папкам. Почему это важно, вы поймете, когда я расскажу о следующем фреймворке.

Второй фреймворк — это 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

ссылка на оригинал статьи https://habr.com/ru/company/exness/blog/489214/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *