Una spruzzata di EarlGrey - Test dell'interfaccia utente dell'app Toptal Talent
Pubblicato: 2022-03-11Una delle cose più importanti che puoi fare come tester per rendere il tuo lavoro più efficiente e veloce è automatizzare l'app che stai testando. Affidarsi esclusivamente ai test manuali non è fattibile poiché è necessario eseguire l'intero set di test ogni giorno, a volte più volte al giorno, testando ogni modifica apportata al codice dell'app.
Questo articolo descriverà il percorso del nostro team per identificare EarlGrey 1.0 di Google come lo strumento che ha funzionato meglio per noi nel contesto dell'automazione dell'app iOS Toptal Talent. Il fatto che lo stiamo utilizzando non significa che EarlGrey sia il miglior strumento di test per tutti: è semplicemente quello che soddisfa le nostre esigenze.
Perché siamo passati a EarlGrey
Nel corso degli anni, il nostro team ha creato diverse app mobili sia su iOS che su Android. All'inizio, abbiamo considerato l'utilizzo di uno strumento di test dell'interfaccia utente multipiattaforma che ci avrebbe consentito di scrivere un unico set di test ed eseguirli su diversi sistemi operativi mobili. Innanzitutto, abbiamo scelto Appium, l'opzione open source più popolare disponibile.
Ma con il passare del tempo, i limiti di Appium divennero sempre più evidenti. Nel nostro caso, i due principali svantaggi di Appium erano:
- La discutibile stabilità del framework ha causato molti test flakes.
- Il processo di aggiornamento relativamente lento ha ostacolato il nostro lavoro.
Per mitigare il primo difetto di Appium, abbiamo scritto tutti i tipi di modifiche al codice e hack per rendere i test più stabili. Tuttavia, non c'era nulla che potessimo fare per affrontare il secondo. Ogni volta che veniva rilasciata una nuova versione di iOS o Android, Appium impiegava molto tempo per recuperare il ritardo. E molto spesso, a causa di molti bug, l'aggiornamento iniziale era inutilizzabile. Di conseguenza, siamo stati spesso costretti a continuare a eseguire i nostri test su una versione della piattaforma precedente o a disattivarli completamente fino a quando non è stato reso disponibile un aggiornamento di Appium funzionante.
Questo approccio era tutt'altro che ideale e, a causa di questi problemi, insieme ad altri che non tratteremo in dettaglio, abbiamo deciso di cercare alternative. I criteri principali per un nuovo strumento di test erano una maggiore stabilità e aggiornamenti più rapidi . Dopo alcune indagini, abbiamo deciso di utilizzare strumenti di test nativi per ciascuna piattaforma.
Quindi, siamo passati a Espresso per il progetto Android ea EarlGrey 1.0 per lo sviluppo iOS. Con il senno di poi, possiamo ora dire che questa è stata una buona decisione. Il tempo “perso” dovuto alla necessità di scrivere e mantenere due diversi set di test, uno per ciascuna piattaforma, è stato più che compensato dal non dover indagare su così tanti test traballanti e dal non avere tempi di inattività sugli aggiornamenti di versione.
Struttura del progetto locale
Dovrai includere il framework nello stesso progetto Xcode dell'app che stai sviluppando. Quindi abbiamo creato una cartella nella directory principale per ospitare i test dell'interfaccia utente. La creazione del file EarlGrey.swift
è obbligatoria durante l'installazione del framework di test e il suo contenuto è predefinito.

EarlGreyBase
è la classe padre per tutte le classi di test. Contiene i metodi generali setUp
e tearDown
, estesi da XCTestCase
. In setUp
, carichiamo gli stub che verranno generalmente utilizzati dalla maggior parte dei test (più avanti sullo stub) e impostiamo anche alcuni flag di configurazione che abbiamo notato aumentare la stabilità dei test:
// 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)
Usiamo il modello di progettazione Page Object: ogni schermata dell'app ha una classe corrispondente in cui sono definiti tutti gli elementi dell'interfaccia utente e le loro possibili interazioni. Questa classe è chiamata "pagina". I metodi di prova sono raggruppati per caratteristiche che risiedono in file e classi separati dalle pagine.
Per darti un'idea migliore di come tutto viene visualizzato, ecco come appaiono le schermate Login e Password dimenticata nella nostra app e come sono rappresentate dagli oggetti pagina.

Più avanti nell'articolo presenteremo il contenuto del codice dell'oggetto Pagina di accesso.
Metodi di utilità personalizzati
Il modo in cui EarlGrey sincronizza le azioni di test con l'app non è sempre perfetto. Ad esempio, potrebbe provare a fare clic su un pulsante che non è ancora caricato nella gerarchia dell'interfaccia utente, causando il fallimento di un test. Per evitare questo problema, abbiamo creato metodi personalizzati per attendere che gli elementi appaiano nello stato desiderato prima di interagire con essi.
Ecco alcuni esempi:
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 }
Un'altra cosa che EarlGrey non sta facendo da solo è scorrere lo schermo finché l'elemento desiderato non diventa visibile. Ecco come possiamo farlo:
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 } }
Altri metodi di utilità mancanti nell'API di EarlGrey che abbiamo identificato sono il conteggio degli elementi e la lettura dei valori di testo. Il codice per queste utilità è disponibile su GitHub: qui e qui.

Stubbing delle chiamate API
Per essere sicuri di evitare falsi risultati dei test causati da problemi del server back-end, utilizziamo la libreria OHHTTPStubs per simulare le chiamate al server. La documentazione sulla loro home page è piuttosto semplice, ma presenteremo come stub le risposte nella nostra app, che utilizza l'API GraphQL.
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 } }
Il caricamento dello stub viene eseguito chiamando il metodo setupOHTTPStub
:
StubsHelper.setupOHTTPStub(for: .login)
Mettere tutto insieme
Questa sezione mostrerà come utilizziamo tutti i principi sopra descritti per scrivere un test di accesso end-to-end effettivo.
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" ) } }
Esecuzione di test in CI
Usiamo Jenkins come nostro sistema di integrazione continua ed eseguiamo i test dell'interfaccia utente per ogni commit in ogni richiesta pull.
Utilizziamo la fastlane scan
per eseguire i test in CI e generare report. È utile avere screenshot allegati a questi rapporti per i test non riusciti. Sfortunatamente, la scan
non fornisce questa funzionalità, quindi abbiamo dovuto personalizzarla.
Nella funzione tearDown()
, rileviamo se il test ha fallito e salviamo uno screenshot del simulatore iOS in caso contrario.
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.") } }
Gli screenshot vengono salvati nella cartella Simulatore e dovrai recuperarli da lì per allegarli come artefatti di build. Usiamo Rake
per gestire i nostri script CI. Ecco come raccogliamo gli artefatti di prova:
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
Da asporto chiave
Se stai cercando un modo veloce e affidabile per automatizzare i tuoi test iOS, non cercare oltre EarlGrey. È sviluppato e mantenuto da Google (c'è bisogno di aggiungere altro?) e per molti aspetti è superiore ad altri strumenti oggi disponibili.
Dovrai armeggiare un po' con il framework per preparare metodi di utilità per promuovere la stabilità del test. Per fare ciò, puoi iniziare con i nostri esempi di metodi di utilità personalizzati.
Ti consigliamo di testare su dati stub per assicurarti che i tuoi test non falliscano perché il server back-end non ha tutti i dati di test che ti aspetteresti. Usa OHHTTPStubs
o un server web locale simile per portare a termine il lavoro.
Quando esegui i test in CI, assicurati di fornire schermate per i casi non riusciti per semplificare il debug.
Forse ti starai chiedendo perché non siamo ancora migrati a EarlGrey 2.0, ed ecco una rapida spiegazione. La nuova versione è stata rilasciata l'anno scorso e promette alcuni miglioramenti rispetto alla v1.0. Sfortunatamente, quando abbiamo adottato EarlGrey, la v2.0 non era particolarmente stabile. Pertanto non siamo ancora passati alla v2.0. Tuttavia, il nostro team attende con impazienza una correzione di bug per la nuova versione in modo da poter migrare la nostra infrastruttura in futuro.
Risorse in linea
La guida introduttiva di EarlGrey sulla home page di GitHub è il punto da cui vuoi iniziare se stai considerando il framework di test per il tuo progetto. Lì troverai una guida all'installazione facile da usare, la documentazione dell'API dello strumento e un pratico cheat sheet che elenca tutti i metodi del framework in un modo semplice da usare durante la scrittura dei test.
Per ulteriori informazioni sulla scrittura di test automatici per iOS, puoi anche consultare uno dei nostri precedenti post sul blog.