A Splash of EarlGrey - Test de l'interface utilisateur de l'application Toptal Talent

Publié: 2022-03-11

L'une des choses les plus importantes que vous puissiez faire en tant que testeur pour rendre votre travail plus efficace et rapide est d'automatiser l'application que vous testez. Il n'est pas possible de s'appuyer uniquement sur des tests manuels, car vous auriez besoin d'exécuter l'ensemble complet de tests tous les jours, parfois plusieurs fois par jour, en testant chaque modification transmise au code de l'application.

Cet article décrira le parcours de notre équipe pour identifier EarlGrey 1.0 de Google comme l'outil qui a le mieux fonctionné pour nous dans le contexte de l'automatisation de l'application iOS Toptal Talent. Le fait que nous l'utilisions ne signifie pas qu'EarlGrey est le meilleur outil de test pour tout le monde - c'est simplement celui qui correspondait à nos besoins.

Pourquoi nous sommes passés à EarlGrey

Au fil des ans, notre équipe a créé différentes applications mobiles sur iOS et Android. Au début, nous avons envisagé d'utiliser un outil de test d'interface utilisateur multiplateforme qui nous permettrait d'écrire un seul ensemble de tests et de les exécuter sur différents systèmes d'exploitation mobiles. Tout d'abord, nous avons opté pour Appium, l'option open source la plus populaire disponible.

Mais au fil du temps, les limites d'Appium sont devenues de plus en plus évidentes. Dans notre cas, les deux principaux inconvénients d'Appium étaient :

  • La stabilité douteuse du cadre a causé de nombreux éclats de test.
  • Le processus de mise à jour relativement lent a entravé notre travail.

Pour atténuer la première lacune d'Appium, nous avons écrit toutes sortes d'ajustements de code et de hacks pour rendre les tests plus stables. Cependant, nous ne pouvions rien faire pour régler le second. Chaque fois qu'une nouvelle version d'iOS ou d'Android sortait, Appium mettait beaucoup de temps à rattraper son retard. Et très souvent, à cause de nombreux bugs, la mise à jour initiale était inutilisable. En conséquence, nous étions souvent obligés de continuer à exécuter nos tests sur une ancienne version de la plate-forme ou de les désactiver complètement jusqu'à ce qu'une mise à jour fonctionnelle d'Appium soit disponible.

Cette approche était loin d'être idéale, et en raison de ces problèmes, ainsi que d'autres que nous n'aborderons pas en détail, nous avons décidé de rechercher des alternatives. Les principaux critères pour un nouvel outil de test étaient une stabilité accrue et des mises à jour plus rapides . Après quelques recherches, nous avons décidé d'utiliser des outils de test natifs pour chaque plate-forme.

Nous sommes donc passés à Espresso pour le projet Android et à EarlGrey 1.0 pour le développement iOS. Avec le recul, on peut maintenant dire que c'était une bonne décision. Le temps "perdu" en raison de la nécessité d'écrire et de maintenir deux ensembles de tests différents, un pour chaque plate-forme, a été plus que compensé par le fait qu'il n'a pas été nécessaire d'enquêter sur autant de tests erronés et qu'il n'y a pas eu de temps d'arrêt sur les mises à jour de version.

Structure locale du projet

Vous devrez inclure le framework dans le même projet Xcode que l'application que vous développez. Nous avons donc créé un dossier dans le répertoire racine pour héberger les tests d'interface utilisateur. La création du fichier EarlGrey.swift est obligatoire lors de l'installation de l'infrastructure de test et son contenu est prédéfini.

Toptal Talent App : structure de projet locale

EarlGreyBase est la classe parente de toutes les classes de test. Il contient les méthodes générales setUp et tearDown , étendues de XCTestCase . Dans setUp , nous chargeons les stubs qui seront généralement utilisés par la plupart des tests (plus d'informations sur le stub plus tard) et nous définissons également certains drapeaux de configuration qui, nous l'avons remarqué, augmentent la stabilité des tests :

 // 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)

Nous utilisons le modèle de conception Page Object - chaque écran de l'application a une classe correspondante où tous les éléments de l'interface utilisateur et leurs interactions possibles sont définis. Cette classe s'appelle une "page". Les méthodes de test sont regroupées par fonctionnalités résidant dans des fichiers et des classes distincts des pages.

Pour vous donner une meilleure idée de la façon dont tout est affiché, voici à quoi ressemblent les écrans de connexion et de mot de passe oublié dans notre application et comment ils sont représentés par les objets de la page.

C'est l'apparence des écrans de connexion et de mot de passe oublié dans notre application.

Plus loin dans l'article, nous présenterons le contenu du code de l'objet Page de connexion.

Méthodes utilitaires personnalisées

La façon dont EarlGrey synchronise les actions de test avec l'application n'est pas toujours parfaite. Par exemple, il peut essayer de cliquer sur un bouton qui n'est pas encore chargé dans la hiérarchie de l'interface utilisateur, provoquant l'échec d'un test. Pour éviter ce problème, nous avons créé des méthodes personnalisées pour attendre que les éléments apparaissent dans l'état souhaité avant d'interagir avec eux.

Voici quelques exemples:

 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 }

Une autre chose qu'EarlGrey ne fait pas tout seul est de faire défiler l'écran jusqu'à ce que l'élément souhaité devienne visible. Voici comment nous pouvons faire cela :

 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 } }

D'autres méthodes utilitaires manquantes dans l'API d'EarlGrey que nous avons identifiées sont le comptage des éléments et la lecture des valeurs textuelles. Le code de ces utilitaires est disponible sur GitHub : ici et ici.

Appels d'API de substitution

Pour nous assurer d'éviter les faux résultats de test causés par des problèmes de serveur principal, nous utilisons la bibliothèque OHHTTPStubs pour simuler les appels de serveur. La documentation sur leur page d'accueil est assez simple, mais nous présenterons comment nous stubons les réponses dans notre application, qui utilise 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 } }

Le chargement du stub est effectué en appelant la méthode setupOHTTPStub :

 StubsHelper.setupOHTTPStub(for: .login)

Tout mettre ensemble

Cette section montrera comment nous utilisons tous les principes décrits ci-dessus pour écrire un véritable test de connexion de bout en bout.

 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" ) } }

Exécution de tests dans CI

Nous utilisons Jenkins comme système d'intégration continue et nous exécutons les tests d'interface utilisateur pour chaque validation dans chaque demande d'extraction.

Nous utilisons fastlane scan pour exécuter les tests en CI et générer des rapports. Il est utile d'avoir des captures d'écran jointes à ces rapports pour les tests ayant échoué. Malheureusement, la scan ne fournit pas cette fonctionnalité, nous avons donc dû la personnaliser.

Dans la fonction tearDown() , nous détectons si le test a échoué et enregistrons une capture d'écran du simulateur iOS si c'est le cas.

 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.") } }

Les captures d'écran sont enregistrées dans le dossier Simulator, et vous devrez les récupérer à partir de là afin de les joindre en tant qu'artefacts de construction. Nous utilisons Rake pour gérer nos scripts CI. Voici comment nous rassemblons les artefacts de test :

 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

Points clés à retenir

Si vous recherchez un moyen rapide et fiable d'automatiser vos tests iOS, ne cherchez pas plus loin que EarlGrey. Il est développé et maintenu par Google (faut-il en dire plus ?) et, à bien des égards, il est supérieur aux autres outils disponibles aujourd'hui.

Vous devrez bricoler un peu le framework pour préparer des méthodes utilitaires afin de favoriser la stabilité des tests. Pour ce faire, vous pouvez commencer par nos exemples de méthodes utilitaires personnalisées.

Nous vous recommandons de tester sur des données stub pour vous assurer que vos tests n'échoueront pas car le serveur principal ne dispose pas de toutes les données de test que vous attendez de lui. Utilisez OHHTTPStubs ou un serveur Web local similaire pour faire le travail.

Lors de l'exécution de vos tests dans CI, assurez-vous de fournir des captures d'écran pour les cas ayant échoué afin de faciliter le débogage.

Vous vous demandez peut-être pourquoi nous n'avons pas encore migré vers EarlGrey 2.0, et voici une explication rapide. La nouvelle version est sortie l'année dernière et promet quelques améliorations par rapport à la v1.0. Malheureusement, lorsque nous avons adopté EarlGrey, la v2.0 n'était pas particulièrement stable. Par conséquent, nous n'avons pas encore effectué la transition vers la v2.0. Cependant, notre équipe attend avec impatience une correction de bogue pour la nouvelle version afin que nous puissions migrer notre infrastructure à l'avenir.

Ressources en ligne

Le guide de démarrage d'EarlGrey sur la page d'accueil GitHub est l' endroit à partir duquel vous souhaitez commencer si vous envisagez le cadre de test pour votre projet. Vous y trouverez un guide d'installation facile à utiliser, la documentation de l'API de l'outil et une feuille de triche pratique répertoriant toutes les méthodes du framework d'une manière simple à utiliser lors de l'écriture de vos tests.

Pour plus d'informations sur l'écriture de tests automatisés pour iOS, vous pouvez également consulter l'un de nos précédents articles de blog.