A Splash of EarlGrey – UI 測試 Toptal Talent 應用程序
已發表: 2022-03-11作為一名測試人員,你可以做的最重要的事情之一就是讓你的工作更高效、更快速地自動化你正在測試的應用程序。 僅僅依靠手動測試是不可行的,因為您需要每天運行全套測試,有時一天多次,測試推送到應用程序代碼的每一個更改。
本文將描述我們的團隊如何將 Google 的 EarlGrey 1.0 確定為在自動化 iOS Toptal Talent 應用程序方面最適合我們的工具。 我們使用它的事實並不意味著 EarlGrey 是適合每個人的最佳測試工具 - 它恰好是適合我們需求的工具。
為什麼我們過渡到 EarlGrey
多年來,我們的團隊在 iOS 和 Android 上構建了不同的移動應用程序。 一開始,我們考慮使用跨平台的 UI 測試工具,它允許我們編寫一組測試並在不同的移動操作系統上執行它們。 首先,我們選擇了 Appium,這是最流行的開源選項。
但隨著時間的推移,Appium 的局限性越來越明顯。 在我們的案例中,Appium 的兩個主要缺點是:
- 該框架有問題的穩定性導致了許多測試片。
- 相對緩慢的更新過程阻礙了我們的工作。
為了減輕 Appium 的第一個缺點,我們編寫了各種代碼調整和技巧,以使測試更加穩定。 然而,我們無法解決第二個問題。 每次發布新版本的 iOS 或 Android 時,Appium 都需要很長時間才能趕上。 很多時候,由於有很多錯誤,初始更新無法使用。 結果,我們經常被迫繼續在舊平台版本上執行我們的測試,或者完全關閉它們,直到有可用的 Appium 更新可用。
這種方法遠非理想,由於這些問題以及我們不會詳細介紹的其他問題,我們決定尋找替代方案。 新測試工具的首要標準是提高穩定性和加快更新速度。 經過一番調查,我們決定為每個平台使用原生測試工具。
因此,我們為 Android 項目轉換為 Espresso,為 iOS 開發轉換為 EarlGrey 1.0。 事後看來,我們現在可以說這是一個很好的決定。 由於需要編寫和維護兩組不同的測試,一個用於每個平台,“損失”的時間完全是由於不需要調查這麼多不穩定的測試並且沒有任何版本更新的停機時間而彌補的。
本地項目結構
您需要將框架包含在與您正在開發的應用程序相同的 Xcode 項目中。 因此,我們在根目錄中創建了一個文件夾來託管 UI 測試。 安裝測試框架時必須創建EarlGrey.swift
文件,其內容是預定義的。

EarlGreyBase
是所有測試類的父類。 它包含從XCTestCase
擴展的通用setUp
和tearDown
方法。 在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)
我們使用頁面對象設計模式——應用程序中的每個屏幕都有一個對應的類,其中定義了所有 UI 元素及其可能的交互。 此類稱為“頁面”。 測試方法按位於頁面中的單獨文件和類中的功能進行分組。
為了讓您更好地了解所有內容的顯示方式,以下是我們應用程序中登錄和忘記密碼屏幕的外觀以及頁面對像如何表示它們。

在本文後面,我們將展示登錄頁面對象的代碼內容。
自定義實用方法
EarlGrey 將測試操作與應用程序同步的方式並不總是完美的。 例如,它可能會嘗試單擊 UI 層次結構中尚未加載的按鈕,從而導致測試失敗。 為了避免這個問題,我們創建了自定義方法來等待元素出現在所需的狀態,然後再與它們進行交互。
這裡有一些例子:
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 } }
我們發現 EarlGrey 的 API 中缺少的其他實用方法是計數元素和讀取文本值。 這些實用程序的代碼可在 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 作為我們的持續集成系統,並在每個拉取請求中為每個提交運行 UI 測試。
我們使用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.") } }
屏幕截圖保存在 Simulator 文件夾中,您需要從那裡獲取它們以便將它們附加為構建工件。 我們使用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
或類似的本地 Web 服務器來完成工作。
在 CI 中運行測試時,請確保提供失敗案例的屏幕截圖,以便於調試。
您可能想知道為什麼我們還沒有遷移到 EarlGrey 2.0,這裡有一個簡單的解釋。 新版本於去年發布,它承諾在 v1.0 上進行一些增強。 不幸的是,當我們採用 EarlGrey 時,v2.0 並不是特別穩定。 因此,我們還沒有過渡到 v2.0。 但是,我們的團隊正在熱切地等待新版本的錯誤修復,以便我們將來可以遷移我們的基礎架構。
在線資源
如果您正在考慮項目的測試框架,那麼 GitHub 主頁上的 EarlGrey 入門指南是您想要開始的地方。 在那裡,您將找到一個易於使用的安裝指南、該工具的 API 文檔和一個方便的備忘單,該備忘單以在編寫測試時易於使用的方式列出了所有框架的方法。
有關為 iOS 編寫自動化測試的更多信息,您還可以查看我們之前的一篇博客文章。