アールグレイのスプラッシュ–ToptalTalentアプリのUIテスト

公開: 2022-03-11

テスターとして作業をより効率的かつ高速にするためにできる最も重要なことの1つは、テストしているアプリを自動化することです。 アプリコードにプッシュされたすべての変更をテストするために、毎日、場合によっては1日に複数回、テストのフルセットを実行する必要があるため、手動テストのみに依存することは現実的ではありません。

この記事では、iOSToptalTalentアプリの自動化のコンテキストでGoogleのEarlGrey1.0を最適に機能するツールとして特定するためのチームの旅について説明します。 私たちがそれを使用しているという事実は、EarlGreyがすべての人にとって最高のテストツールであることを意味するわけではありません-それはたまたま私たちのニーズに合ったものです。

アールグレイに移行した理由

長年にわたり、私たちのチームはiOSとAndroidの両方でさまざまなモバイルアプリを構築してきました。 最初は、クロスプラットフォームのUIテストツールを使用して、単一のテストセットを作成し、さまざまなモバイルオペレーティングシステムで実行できるようにすることを検討しました。 まず、利用可能な最も人気のあるオープンソースオプションであるAppiumを使用しました。

しかし、時間が経つにつれて、Appiumの制限はますます明白になりました。 私たちの場合、Appiumの2つの主な欠点は次のとおりです。

  • フレームワークの疑わしい安定性は、多くのテストフレークを引き起こしました。
  • 更新プロセスが比較的遅いため、作業が妨げられました。

Appiumの最初の欠点を軽減するために、テストをより安定させるために、あらゆる種類のコードの微調整とハックを作成しました。 しかし、2番目に対処するために私たちにできることは何もありませんでした。 iOSまたはAndroidの新しいバージョンがリリースされるたびに、Appiumは追いつくのに長い時間がかかりました。 そして、多くの場合、多くのバグがあるため、最初の更新は使用できませんでした。 その結果、古いプラットフォームバージョンでテストを実行し続けるか、有効なAppiumアップデートが利用可能になるまでテストを完全にオフにすることを余儀なくされることがよくありました。

このアプローチは理想からはほど遠いものでした。これらの問題と、詳細には説明しない追加の問題があるため、代替案を探すことにしました。 新しいテストツールの最重要基準は、安定性の向上と更新の高速化でした。 調査の結果、プラットフォームごとにネイティブテストツールを使用することにしました。

そこで、AndroidプロジェクトではEspressoに、iOS開発ではEarlGrey1.0に移行しました。 後から考えると、これは良い決断だったと言えます。 プラットフォームごとに1つずつ、2つの異なるテストセットを作成して維持する必要があるために「失われた」時間は、多くの不安定なテストを調査する必要がなく、バージョンの更新でダウンタイムが発生しないことで埋め合わせられました。

ローカルプロジェクト構造

開発しているアプリと同じXcodeプロジェクトにフレームワークを含める必要があります。 そこで、UIテストをホストするためにルートディレクトリにフォルダを作成しました。 テストフレームワークをインストールする場合、 EarlGrey.swiftファイルの作成は必須であり、その内容は事前定義されています。

Toptal Talent App:ローカルプロジェクト構造

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要素とそれらの可能な相互作用が定義されている対応するクラスがあります。 このクラスは「ページ」と呼ばれます。 テストメソッドは、ページとは別のファイルおよびクラスにある機能ごとにグループ化されています。

すべてがどのように表示されるかをよりよく理解するために、これは、ログイン画面とパスワードを忘れた画面がアプリでどのように表示され、ページオブジェクトによってどのように表されるかを示しています。

これは、アプリのログイン画面とパスワードを忘れた場合の画面の外観です。

この記事の後半で、ログインページオブジェクトのコードコンテンツを紹介します。

カスタムユーティリティメソッド

アールグレイがテストアクションをアプリと同期する方法は、必ずしも完璧ではありません。 たとえば、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 }

アールグレイが単独で行っていないもう1つのことは、目的の要素が表示されるまで画面をスクロールすることです。 これを行う方法は次のとおりです。

 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ライブラリを使用してサーバー呼び出しをモックします。 彼らのホームページのドキュメントは非常に単純ですが、GraphQLAPIを使用するアプリで応答をスタブする方法を示します。

 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でテストを実行するときは、デバッグを容易にするために、失敗したケースのスクリーンショットを必ず提供してください。

なぜEarlGrey2.0に移行しなかったのか疑問に思われるかもしれませんが、ここで簡単に説明します。 新しいバージョンは昨年リリースされ、v1.0よりもいくつかの機能強化が約束されています。 残念ながら、EarlGreyを採用したとき、v2.0は特に安定していませんでした。 したがって、まだv2.0に移行していません。 ただし、将来的にインフラストラクチャを移行できるように、私たちのチームは新しいバージョンのバグ修正を熱心に待っています。

オンラインリソース

GitHubホームページにあるEarlGreyのスタートガイドはプロジェクトのテストフレームワークを検討している場合に開始したい場所です。 そこには、使いやすいインストールガイド、ツールのAPIドキュメント、およびテストの作成中に簡単に使用できる方法ですべてのフレームワークのメソッドをリストした便利なチートシートがあります。

iOSの自動テストの作成に関する追加情報については、以前のブログ投稿の1つを確認することもできます。