Всплеск EarlGrey — тестирование пользовательского интерфейса приложения Toptal Talent

Опубликовано: 2022-03-11

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

В этой статье будет описан путь нашей команды к определению Google EarlGrey 1.0 как инструмента, который лучше всего работал для нас в контексте автоматизации приложения Toptal Talent для iOS. Тот факт, что мы его используем, не означает, что EarlGrey — лучший инструмент для тестирования для всех — просто он подходит для наших нужд.

Почему мы перешли на EarlGrey

На протяжении многих лет наша команда создавала различные мобильные приложения для iOS и Android. Вначале мы рассматривали возможность использования кроссплатформенного инструмента тестирования пользовательского интерфейса, который позволил бы нам написать единый набор тестов и выполнять их в разных мобильных операционных системах. Во-первых, мы выбрали Appium, самый популярный вариант с открытым исходным кодом.

Но со временем ограничения Appium становились все более и более очевидными. В нашем случае двумя основными недостатками Appium были:

  • Сомнительная стабильность фреймворка вызвала множество фальшивых тестов.
  • Сравнительно медленный процесс обновления мешал нашей работе.

Чтобы смягчить первый недостаток Appium, мы написали всевозможные настройки кода и хаки, чтобы сделать тесты более стабильными. Однако мы ничего не могли сделать со вторым. Каждый раз, когда выходила новая версия iOS или Android, Appium требовалось много времени, чтобы наверстать упущенное. И очень часто из-за множества ошибок начальное обновление было непригодным для использования. В результате нам часто приходилось продолжать выполнять наши тесты на более старой версии платформы или полностью отключать их до тех пор, пока не станет доступным работающее обновление Appium.

Этот подход был далек от идеала, и из-за этих проблем, а также дополнительных, которые мы не будем подробно освещать, мы решили искать альтернативы. Главными критериями для нового инструмента тестирования были повышенная стабильность и более быстрые обновления . После некоторого исследования мы решили использовать собственные инструменты тестирования для каждой платформы.

Итак, мы перешли на Espresso для проекта Android и на EarlGrey 1.0 для разработки iOS. Теперь, оглядываясь назад, мы можем сказать, что это было правильное решение. Время, «потерянное» из-за необходимости написания и поддержки двух разных наборов тестов, по одному для каждой платформы, было более чем компенсировано отсутствием необходимости исследовать так много ненадежных тестов и отсутствием простоев при обновлении версий.

Локальная структура проекта

Вам нужно будет включить фреймворк в тот же проект Xcode, что и разрабатываемое вами приложение. Поэтому мы создали папку в корневом каталоге для размещения тестов пользовательского интерфейса. Создание файла EarlGrey.swift является обязательным при установке среды тестирования, и его содержимое предопределено.

Приложение Toptal Talent: локальная структура проекта

EarlGreyBase является родительским классом для всех тестовых классов. Он содержит общие setUp и tearDown , расширенные из XCTestCase . В setUp мы загружаем заглушки, которые обычно используются в большинстве тестов (подробнее о заглушках позже), а также устанавливаем некоторые конфигурационные флаги, которые, как мы заметили, повышают стабильность тестов:

 // Turn off EarlGrey's network requests tracking since we don't use it and it can block tests execution GREYConfiguration.sharedInstance().setValue([".*"], forConfigKey: kGREYConfigKeyURLBlacklistRegex) GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeyAnalyticsEnabled)

Мы используем шаблон проектирования Page Object — каждый экран в приложении имеет соответствующий класс, в котором определены все элементы пользовательского интерфейса и их возможные взаимодействия. Этот класс называется «страницей». Методы тестирования сгруппированы по функциям, находящимся в отдельных файлах и классах со страниц.

Чтобы дать вам лучшее представление о том, как все отображается, вот как выглядят экраны «Вход» и «Забыли пароль» в нашем приложении и как они представлены объектами страницы.

Это внешний вид экранов «Войти» и «Забыли пароль» в нашем приложении.

Далее в статье мы представим содержимое кода объекта страницы входа.

Пользовательские служебные методы

То, как EarlGrey синхронизирует тестовые действия с приложением, не всегда идеально. Например, он может попытаться щелкнуть кнопку, которая еще не загружена в иерархию пользовательского интерфейса, что приведет к сбою теста. Чтобы избежать этой проблемы, мы создали пользовательские методы, которые ждут, пока элементы не появятся в желаемом состоянии, прежде чем взаимодействовать с ними.

Вот несколько примеров:

 static func asyncWaitForVisibility(on element: GREYInteraction) { // By default, EarlGrey blocks test execution while // the app is animating or doing anything in the background. //https://github.com/google/EarlGrey/blob/master/docs/api.md#synchronization GREYConfiguration.sharedInstance().setValue(false, forConfigKey: kGREYConfigKeySynchronizationEnabled) element.assert(grey_sufficientlyVisible()) GREYConfiguration.sharedInstance().setValue(true, forConfigKey: kGREYConfigKeySynchronizationEnabled) } static func waitElementVisibility(for element: GREYInteraction, timeout: Double = 15.0) -> Bool { GREYCondition(name: "Wait for element to appear", block: { var error: NSError? element.assert(grey_notNil(), error: &error) return error == nil }).wait(withTimeout: timeout, pollInterval: 0.5) if !elementVisible(element) { XCTFail("Element didn't appear") } return true }

Еще одна вещь, которую EarlGrey не делает сама по себе, — это прокрутка экрана до тех пор, пока нужный элемент не станет видимым. Вот как мы можем это сделать:

 static func elementVisible(_ element: GREYInteraction) -> Bool { var error: NSError? element.assert(grey_notVisible(), error: &error) if error != nil { return true } else { return false } } static func scrollUntilElementVisible(_ scrollDirection: GREYDirection, _ speed: String, _ searchedElement: GREYInteraction, _ actionElement: GREYInteraction) -> Bool { var swipes = 0 while !elementVisible(searchedElement) && swipes < 10 { if speed == "slow" { actionElement.perform(grey_swipeSlowInDirection(scrollDirection)) } else { actionElement.perform(grey_swipeFastInDirection(scrollDirection)) } swipes += 1 } if swipes >= 10 { return false } else { return true } }

Другими обнаруженными нами вспомогательными методами, отсутствующими в API EarlGrey, являются подсчет элементов и чтение текстовых значений. Код этих утилит доступен на GitHub: здесь и здесь.

Заглушки вызовов API

Чтобы убедиться, что мы избегаем ложных результатов тестирования, вызванных проблемами внутреннего сервера, мы используем библиотеку OHHTTPStubs для имитации вызовов сервера. Документация на их домашней странице довольно проста, но мы покажем, как мы заглушаем ответы в нашем приложении, которое использует GraphQL API.

 class StubsHelper { static let testURL = URL(string: "https://[our backend server]")! static func setupOHTTPStub(for request: StubbedRequest, delayed: Bool = false) { stub(condition: isHost(testURL.host!) && hasJsonBody(request.bodyDict())) { _ in let fix = appFixture(forRequest: request) if delayed { return fix.requestTime(0.1, responseTime: 7.0) } else { return fix } } } static let stubbedEmail = "[email protected]" static let stubbedPassword = "password" enum StubbedRequest { case login func bodyDict() -> [String: Any] { switch self { case .login: return EmailPasswordSignInMutation( email: stubbedTalentLogin, password: stubbedTalentPassword ).makeBodyIdentifier() } } func statusCode() -> Int32 { return 200 } func jsonFileName() -> String { let fileName: String switch self { case .login: fileName = "login" } return "\(fileName).json" } } private extension GraphQLOperation { func makeBodyIdentifier() -> [String: Any] { let body: GraphQLMap = [ "query": queryDocument, "variables": variables, "operationName": operationName ] // Normalize values like enums here, otherwise body comparison will fail guard let normalizedBody = body.jsonValue as? [String: Any] else { fatalError() } return normalizedBody } }

Загрузка заглушки выполняется вызовом метода setupOHTTPStub :

 StubsHelper.setupOHTTPStub(for: .login)

Собираем все вместе

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

 import EarlGrey final class LoginPage { func login() -> HomePage { fillLoginForm() loginButton().perform(grey_tap()) return HomePage() } func fillLoginForm() { ElementsHelper.waitElementVisibility(emailField()) emailField().perform(grey_replaceText(StubsHelper.stubbedTalentLogin)) passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText(StubsHelper.stubbedTalentPassword)) } func clearAllInputs() { if ElementsHelper.elementVisible(passwordField()) { passwordField().perform(grey_tap()) passwordField().perform(grey_replaceText("")) } emailField().perform(grey_tap()) emailField().perform(grey_replaceText("")) } } private extension LoginPage { func emailField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityLabel("Email"), file: file, line: line) } func passwordField(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement( with: grey_allOf([ grey_accessibilityLabel("Password"), grey_sufficientlyVisible(), grey_userInteractionEnabled() ]), file: file, line: line ) } func loginButton(file: StaticString = #file, line: UInt = #line) -> GREYInteraction { return EarlGrey.selectElement(with: grey_accessibilityID("login_button"), file: file, line: line) } } class BBucketTests: EarlGreyBase { func testLogin() { StubsHelper.setupOHTTPStub(for: .login) LoginPage().clearAllInputs() let homePage = LoginPage().login() GREYAssertTrue( homePage.assertVisible(), reason: "Home screen not displayed after successful login" ) } }

Запуск тестов в CI

Мы используем Jenkins в качестве системы непрерывной интеграции и запускаем тесты пользовательского интерфейса для каждой фиксации в каждом запросе на включение.

Мы используем fastlane scan для выполнения тестов в CI и создания отчетов. Полезно прикреплять к этим отчетам снимки экрана для неудачных тестов. К сожалению, scan не предоставляет такой функциональности, поэтому нам пришлось сделать ее на заказ.

В функции tearDown() мы определяем, не прошел ли тест, и сохраняем скриншот симулятора iOS, если это не так.

 import EarlGrey import XCTest import UIScreenCapture override func tearDown() { if testRun!.failureCount > 0 { // name is a property of the XCTest instance // https://developer.apple.com/documentation/xctest/xctest/1500990-name takeScreenshotAndSave(as: name) } super.tearDown() } func takeScreenshotAndSave(as testCaseName: String) { let imageData = UIScreenCapture.takeSnapshotGetJPEG() let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let filePath = "\(paths[0])/\(testCaseName).jpg" do { try imageData?.write(to: URL.init(fileURLWithPath: filePath)) } catch { XCTFail("Screenshot not written.") } }

Скриншоты сохраняются в папке Симулятора, и вам нужно будет получить их оттуда, чтобы прикрепить как артефакты сборки. Мы используем Rake для управления нашими сценариями CI. Вот как мы собираем тестовые артефакты:

 def gather_test_artifacts(booted_sim_id, destination_folder) app_container_on_sim = `xcrun simctl get_app_container #{booted_sim_id} [your bundle id] data`.strip FileUtils.cp_r "#{app_container_on_sim}/Documents", destination_folder end

Ключевые выводы

Если вы ищете быстрый и надежный способ автоматизации тестирования iOS, вам подойдет EarlGrey. Он разработан и поддерживается Google (нужно ли говорить больше?), и во многих отношениях он превосходит другие инструменты, доступные сегодня.

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

Мы рекомендуем проводить тестирование на заглушенных данных, чтобы убедиться, что ваши тесты не дадут сбой, потому что на внутреннем сервере нет всех тестовых данных, которые вы ожидаете от него. Используйте OHHTTPStubs или аналогичный локальный веб-сервер, чтобы выполнить работу.

При выполнении тестов в CI не забудьте предоставить скриншоты неудачных случаев, чтобы упростить отладку.

Вам может быть интересно, почему мы еще не перешли на EarlGrey 2.0, и вот краткое объяснение. Новая версия была выпущена в прошлом году и обещает некоторые улучшения по сравнению с версией 1.0. К сожалению, когда мы внедрили EarlGrey, версия 2.0 не отличалась особой стабильностью. Поэтому мы еще не перешли на версию 2.0. Однако наша команда с нетерпением ожидает исправления ошибок в новой версии, чтобы в будущем мы могли перенести нашу инфраструктуру.

Интернет-ресурсы

Руководство EarlGrey по началу работы на домашней странице GitHub — это то место, с которого вы хотите начать, если вы рассматриваете среду тестирования для своего проекта. Там вы найдете простое в использовании руководство по установке, документацию по API инструмента и удобную памятку, в которой перечислены все методы фреймворка в форме, которую легко использовать при написании тестов.

Дополнительную информацию о написании автоматических тестов для iOS вы также можете найти в одном из наших предыдущих постов в блоге.