Современный подход к тестированию локализации на 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

    Exness
    Финтех-компания, признанный лидер индустрии

    Комментарии 5

      +1
      Это, конечно, все хорошо… но невлезающие переводы — это небольшая часть проблем, которая довольно легко обнаруживается.
      А вот что хуже — это проверка качества перевода. Это как-то контролируется?
      Я имею в виду ошибки перевода вроде
      image
      На английском оно, может, было понятно и слова там разные Discard/Cancel/Undo, а при переводе получилось то, что получилось…
        0
        У нас это зона ответственности менеджеров по локализации. Команда разработки пока владеет только английским, русским и украинским языками. Если мы видим подобные ошибки на языках, которые мы знаем, то правим. Ошибки перевода на тайский, китайский и прочие языки, мы, к сожалению, со своей стороны проверить не можем
        0

        Заинтересовало сравнение тайской и вьетнамской версии. Только в тайской названия банков отображались на английском, или во вьетнамской версии банки тоже были на английском?
        Часто говорят, что на вьетнамский необязательно полностью переводить, кажется, они английский нормально знают. Интересно, так ли это?

          0
          Для вьетнамской локали у нас перевод названий банков не так актуален как для тайской. В Тайланде гораздо больше локальных платежных систем чем во Вьетнаме.
          А касаемо частичного перевода интерфейса на тот или иной язык, мне кажется это так себе решение. Экономия небольшая, условно заплатили переводчику за 400 строк вместо 600, но зато в итоге получили приложение, которое выглядит недоделанным.
            0

            Спасибо за ответ! Да, наверное для вашей сферы экономить на локализации неоправданно, люди хотят быть уверенными в приложении, через которое проходят их деньги. А я сужу по играм и просто приложениям, там пользователи некоторых стран закрывают глаза на неполную локализацию :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое