A Splash of EarlGrey – UI ทดสอบแอพ Toptal Talent

เผยแพร่แล้ว: 2022-03-11

สิ่งสำคัญที่สุดอย่างหนึ่งที่คุณสามารถทำได้ในฐานะผู้ทดสอบเพื่อให้งานของคุณมีประสิทธิภาพและรวดเร็วยิ่งขึ้นคือการทำให้แอปที่คุณกำลังทดสอบเป็นแบบอัตโนมัติ การใช้การทดสอบด้วยตนเองเพียงอย่างเดียวนั้นไม่สามารถทำได้ เนื่องจากคุณจะต้องเรียกใช้การทดสอบทั้งชุดทุกวัน บางครั้งวันละหลายครั้ง การทดสอบทุกการเปลี่ยนแปลงที่ส่งไปยังโค้ดของแอป

บทความนี้จะอธิบายการเดินทางของทีมของเราในการระบุ EarlGrey 1.0 ของ Google ว่าเป็นเครื่องมือที่ทำงานได้ดีที่สุดสำหรับเราในบริบทของการทำให้แอป iOS Toptal Talent เป็นอัตโนมัติ ความจริงที่ว่าเราใช้มันไม่ได้หมายความว่า EarlGrey เป็นเครื่องมือทดสอบที่ดีที่สุดสำหรับทุกคน - มันเป็นเครื่องมือที่เหมาะกับความต้องการของเรา

ทำไมเราถึงเปลี่ยนไปใช้เอิร์ลเกรย์

ตลอดหลายปีที่ผ่านมา ทีมงานของเราได้สร้างแอพมือถือที่แตกต่างกันทั้งบน iOS และ Android ในตอนเริ่มต้น เราได้พิจารณาใช้เครื่องมือทดสอบ UI ข้ามแพลตฟอร์ม ซึ่งจะทำให้เราสามารถเขียนการทดสอบชุดเดียวและดำเนินการบนระบบปฏิบัติการมือถือที่แตกต่างกันได้ อันดับแรก เราใช้ Appium ซึ่งเป็นตัวเลือกโอเพนซอร์ซที่ได้รับความนิยมมากที่สุด

แต่เมื่อเวลาผ่านไป ข้อ จำกัด Appium ก็ชัดเจนขึ้นเรื่อยๆ ในกรณีของเรา ข้อเสียหลักสองประการของ Appium คือ:

  • ความเสถียรที่น่าสงสัยของเฟรมเวิร์กทำให้เกิดการทดสอบสะเก็ดหลายครั้ง
  • กระบวนการอัปเดตที่ค่อนข้างช้าขัดขวางงานของเรา

เพื่อลดข้อบกพร่องของ Appium ครั้งแรก เราได้เขียนการปรับแต่งโค้ดและการแฮ็กทุกประเภทเพื่อให้การทดสอบมีเสถียรภาพมากขึ้น อย่างไรก็ตาม เราไม่สามารถแก้ไขข้อที่สองได้ ทุกครั้งที่มีการเปิดตัว iOS หรือ Android เวอร์ชันใหม่ Appium ใช้เวลานานในการติดตาม และบ่อยครั้ง เนื่องจากมีข้อบกพร่องมากมาย การอัปเดตครั้งแรกจึงใช้ไม่ได้ ด้วยเหตุนี้ เราจึงมักถูกบังคับให้ดำเนินการทดสอบของเราบนแพลตฟอร์มเวอร์ชันเก่าหรือปิดโดยสมบูรณ์จนกว่าจะมีการอัปเดต Appium ที่ใช้งานได้

แนวทางนี้อยู่ไกลจากอุดมคติ และด้วยปัญหาเหล่านี้ ร่วมกับวิธีอื่นๆ ที่เราจะไม่กล่าวถึงในรายละเอียด เราจึงตัดสินใจมองหาทางเลือกอื่น เกณฑ์สูงสุดสำหรับเครื่องมือทดสอบใหม่คือ ความเสถียรที่เพิ่มขึ้น และ การอัปเดตที่รวดเร็วขึ้น หลังจากการตรวจสอบ เราตัดสินใจใช้เครื่องมือทดสอบแบบเนทีฟสำหรับแต่ละแพลตฟอร์ม

ดังนั้นเราจึงเปลี่ยนไปใช้ Espresso สำหรับโครงการ Android และ EarlGrey 1.0 สำหรับการพัฒนา iOS เมื่อมองย้อนกลับไป ตอนนี้เราสามารถพูดได้ว่านี่เป็นการตัดสินใจที่ดี เวลาที่ “สูญเสียไป” เนื่องจากความจำเป็นในการเขียนและบำรุงรักษาชุดการทดสอบที่แตกต่างกันสองชุด ชุดละชุดสำหรับแต่ละแพลตฟอร์ม มากกว่าที่ประกอบขึ้นโดยไม่จำเป็นต้องตรวจสอบการทดสอบที่ไม่สม่ำเสมอจำนวนมากและไม่มีการหยุดทำงานใดๆ ในการอัปเดตเวอร์ชัน

โครงสร้างโครงการในพื้นที่

คุณจะต้องรวมเฟรมเวิร์กไว้ในโปรเจ็กต์ Xcode เดียวกันกับแอปที่คุณกำลังพัฒนา ดังนั้นเราจึงสร้างโฟลเดอร์ในไดเร็กทอรีรากเพื่อโฮสต์การทดสอบ UI การสร้างไฟล์ EarlGrey.swift เป็นข้อบังคับเมื่อติดตั้งเฟรมเวิร์กการทดสอบและกำหนดเนื้อหาไว้ล่วงหน้า

แอพ Toptal Talent: โครงสร้างโครงการในพื้นที่

EarlGreyBase เป็นคลาสหลักสำหรับคลาสทดสอบทั้งหมด ประกอบด้วยเมธอด tearDown setUp ซึ่งขยายจาก XCTestCase ใน setUp เราโหลด stub ที่โดยทั่วไปจะใช้โดยการทดสอบส่วนใหญ่ (เพิ่มเติมเกี่ยวกับ stubbing ในภายหลัง) และเรายังตั้งค่าสถานะการกำหนดค่าบางอย่างที่เราสังเกตเห็นว่าเพิ่มความเสถียรของการทดสอบ:

 // 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 - แต่ละหน้าจอในแอปมีคลาสที่สอดคล้องกันซึ่งมีการกำหนดองค์ประกอบ 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 } }

วิธียูทิลิตี้อื่น ๆ ที่ขาดหายไปจาก 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)

รวมทุกอย่างไว้ด้วยกัน

ส่วนนี้จะสาธิตวิธีที่เราใช้หลักการทั้งหมดที่อธิบายไว้ข้างต้นเพื่อเขียนการทดสอบการเข้าสู่ระบบแบบ end-to-end ที่เกิดขึ้นจริง

 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 หรือเว็บเซิร์ฟเวอร์ในพื้นที่ที่คล้ายกันเพื่อทำงานให้เสร็จ

เมื่อรันการทดสอบของคุณใน CI อย่าลืมจัดเตรียมภาพหน้าจอสำหรับกรณีที่ล้มเหลวเพื่อให้การดีบักง่ายขึ้น

คุณอาจสงสัยว่าเหตุใดเราจึงไม่ย้ายไปที่ EarlGrey 2.0 และนี่คือคำอธิบายสั้นๆ เวอร์ชันใหม่เปิดตัวเมื่อปีที่แล้วและสัญญาว่าจะมีการปรับปรุงบางอย่างมากกว่า v1.0 โชคไม่ดีที่เมื่อเรานำ EarlGrey มาใช้ v2.0 นั้นไม่เสถียรเป็นพิเศษ ดังนั้น เราจึงยังไม่เปลี่ยนไปใช้ v2.0 ในตอนนี้ อย่างไรก็ตาม ทีมงานของเรากำลังรอคอยการแก้ไขจุดบกพร่องสำหรับเวอร์ชันใหม่อย่างใจจดใจจ่อ เพื่อให้เราสามารถย้ายโครงสร้างพื้นฐานของเราได้ในอนาคต

แหล่งข้อมูลออนไลน์

คู่มือเริ่มต้นใช้งานของ EarlGrey ในหน้าแรกของ GitHub คือ ที่ ที่คุณต้องการเริ่มต้น หากคุณกำลังพิจารณากรอบงานการทดสอบสำหรับโครงการของคุณ คุณจะพบคู่มือการติดตั้งที่ใช้งานง่าย เอกสาร API ของเครื่องมือ และเอกสารสรุปข้อมูลที่มีประโยชน์ซึ่งแสดงรายการวิธีการทั้งหมดของเฟรมเวิร์กในลักษณะที่ใช้งานง่ายขณะเขียนการทดสอบ

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการเขียนการทดสอบอัตโนมัติสำหรับ iOS คุณสามารถดูโพสต์บล็อกก่อนหน้าของเราได้