Si vous écrivez des tests d’interface utilisateur sur iOS/iPadOS, vous avez certainement constaté que, pour d’obscures raisons, c’est une véritable lutte.

Oui, vous pouvez touchez un toggle, mais le faire en son centre n’est généralement ni utile, ni efficace. Voyons comment remédier à cela, puisqu’Apple semble trainer des pieds pour corriger ce problème.

Aperçu

Vous avez probablement constaté que se contenter d’utiliser tap() sur un interrupteur ou un toggle XCUIElement n’est généralement pas suivi de l’effet escompté. Je vais utiliser un projet très basique pour illustrer comment vous pouvez écrire un test UI qui fonctionnera pour les toggles.

La fonction tap() ne fonctionne pas parce que le point de contact va interagir avec le centre de la vue, qui est vide la plupart du temps ou contient l’étiquette textuelle de l’interrupteur.

Toucher le centre d'un toggle n'a normalement pas d'effet'

Ce projet met en œuvre une vue limitée à deux bascules : l’une est désactivée, l’autre est activée. Cette application ne fait rien, le but étant d’implémenter des tests d’IU pour vérifier l’état des toggles, puis les changer et vérifier le résultat.

Pour faire fonctionner mes tests, j’ai implémenté quelques fonctions, sous la forme d’une extension XCTestCase, afin de déterminer le bon point de contact au centre de l’interrupteur, interagir avec lui, et vérifier l’état du toggle.

NOTE:

Le code source du project est à votre disposition sur mon dépôt GitHub. Téléchargez le ou lisez simplement le code.

Et puisque cet article parle de tests d’interface, je vais en profiter pour présenter rapidement mes conseils d’implémentation sur une façon efficace de les rédiger.

Étendre XCTestCase

L’extension est définie dans le fichier XCTestCase+toggle.swift.

Vérifier l’état d’un toggle

Mon implémentation propose deux techniques pour vérifier l’état d’un toggle :

  1. de façon synchrone lorsque vous savez que le toggle est supposé déjà être dans le bon état.
  2. en vous appuyant sur le système d’expectation dans une recherche asynchrone.

Je vous propose deux fonctions pour vérifier l’état action d’un toggle :

  • checkToggleIsOn(_ toggle: XCUIElement)
  • checkToggleIsOff(_ toggle: XCUIElement)

Chacune va faire échouer le test si l’état ne correspond pas.

En revanche, si vous avez de vous appuyer sur une attente asynchrone, par exemple pour vérifier qu’une action a bien modifié l’état du toggle, vous pouvez alors vous tourner vers ces deux autres fonctions :

  • waitForSwitchToBeOn(_ element: XCUIElement, timeOut : TimeInterval)
  • waitForSwitchToBeOff(_ element: XCUIElement, timeOut : TimeInterval)

Chacune utilise une recherche d’élément utilisant un prédicat et qui attend que l’expectation soit satisfaite dans un intervalle de temps déterminé.

Interaction avec un toggle

Vous disposez de deux méthodes, une pour chaque changement d’état, pour simuler une interaction de l’utilisateur avec un toggle :

  • turnSwitchOn(_ toggle: XCUIElement)
  • turnSwitchOff(_ toggle: XCUIElement)

Chacune des ces fonctions suit la même logique :

  1. la coordonnée du point de contact est calculée comme étant le centre du switch contenu dans le toggle.
  2. Le toggle est touché en premier. J’admets bien volontiers ne pas comprendre pourquoi cette action intermédiaire est nécessaire, mais sans elle la seconde n’aura aucun effet.
  3. Un touché au point central du switch, calculé dans la première étape.
  4. Enfin, un appel à une des fonctions waitForSwitchTobeXxx() pour vérifier que l’état est bien modifié.

Vous devez toucher le centre du _switch_

Si quelqu’un est capable de m’expliquer pour quelle raison le premier touché est indispensable, je serais plus que ravis de le comprendre !

Implémenter des tests IU

Mes tests suivent deux principes élémentaires :

  • Pour rechercher les éléments dans l’interface, je n’utilise que les identifiants d’accessibilité.
  • Mes tests ne manipulent les vues qu’à travers un objet intermédiaire : le proxy d’une vue.

Organiser les identifiants d’accessibilité

Vous pourriez vous demander pourquoi vous devriez vous fier davantage aux identifiants qu’aux étiquettes des éléments pour écrire des tests d’interface utilisateur.

Tout d’abord, l’utilisation des labels ne fonctionnera pas correctement avec les traductions, car le texte change pour chaque localisation testée. Tester votre application dans une seule langue pourrait cacher certains bugs spécifiques et ce n’est probablement pas une bonne idée. Pire encore, le fait de devoir adapter vos tests d’interface utilisateur pour pouvoir tester toutes les langues supportées par votre application va rendre la maintenance de vos tests plutôt pénible.

Vous pourriez utiliser des tags pour trouver des éléments, mais ce ne sont que des chiffres. La gestion des collisions entre les vues deviendra rapidement un cauchemar, et vous ne voulez probablement pas suivre ce chemin.

Utiliser l’identifiant d’accessibilité est une bonne façon de mettre en œuvre des tests d’interface utilisateur fiables et faciles à maintenir. Et même si cela ne suffit pas, c’est aussi la première étape pour garantir que votre application sera plus accessible.

Mettre en œuvre les identifiants d’accessibilité

Avec SwiftUI, vous devez utiliser un modificateur dédié pour associer un identifiant d’accessibilité à une vue :

Toggle("Libellé", isOn: $toggleState)
  .accessibilityIdentifier("id-element")

Il est évident que vous pourriez vous contenter de laisser l’identifiant, en dur, dans votre code. Mais, étant donné qu’ils devront également être utilisés dans vos tests, il est certainement plus sage de les partager par l’intermédiaire d’une énumération de textes.

Une implémentation des plus simple est d’utiliser un enum de type String pour lister ces identifiants.

Le partage des identifiants entre l’application et la cible de test est réalisé avec un modèle simple :

Si je dois tester la vue SomeView, alors :

  • les identifiants sont définis comme les membres d’un enum SomeViewA11y,
  • le fichier déclarant cet enum doit être inclus dans chacune des cibles de l’application et des tests.

Pour une classe de la vue SomeView class, les identifiants d’accessibilité sont les membres d’une énumération_SomeViewA11y_.

public enum SomeViewA11y: String {
  case element1 = "someView/element1"
  
}

J’imagine sans aucun problème qu’il existe une meilleure option, mais ce modèle correspond à mes besoins.

N’hésitez pas à me proposer des solutions alternatives.

Mise en œuvre d’un proxy de vue

Qu’est-ce que j’appelle un proxy de vue ?

Mon proxy de vue est un moyen simple, à partir de mes tests, pour accéder, et manipuler, les éléments qui constituent ma vue.

Il doit contenir :

  • des accesseurs aux sous-vues ;
  • des fonctions de vérification, lorsque cela est nécessaire, pour vérifier l’état de ma vue ou de ses sous-vues ;
  • des fonctions qui implémentent des actions sur ma vue ou ses sous-vues.

Pourquoi requérir un proxy pour chaque vue de mon app ?

Pour la simple raison que j’aime, autant que possible, éviter de répéter mon code, tout particulièrement s’il est susceptible de changer. Si je mets à jour ma vue, je sais que les impacts seront pour l’essentiel, limité à l’objet intermédiaire et éventuellement quelques tests d’IU.

En m’appuyant sur une fine abstraction de mes vues, je peux limiter l’impact des modifications de ma vue et éviter de devoir réaliser de trop gros changements dans mes tests.

La mise en œuvre

Pour une classe SomeView, je vais implémenter une classe SomeViewTestProxy.

import XCTest

public struct SomeViewTestProxy {
  /// Définissez cette propriété si vous voulez ou avez besoin de cibler l'application
  let app: XCUIApplication

  /// Si la vue testée n'est pas la racine de l'app,
  /// référencez la pour limiter la porté de vos recherches de sous-vues.
  let view: XCUIElement

  /// Accesseur sur une sous-vue
  var subView: XCUIElement {
    view.element(matching _some_type_, identifier: SomeViewA11y.subView.rawValue)
  }

  /// Vérifier un état
  func checkSomething(...) { ... }

  /// Réaliser une action sur la vue
  func performSomeAction(...) { ... }

  ...
}

Ce second modèle de conception mérite certainement des améliorations, mais cela sort largement du sujet de cet article.

Pour l’application exempte, vous pouvez consulter le source ContentViewTestProxy dans le projet pour les détails d’implémentation.

Commencez le code de votre scénario de test

Mon test va relancer l’application pour chaque test.

À chaque démarrage de l’app, j’en' garde la référence dans une propriété.

final class suiToggleUITests: XCTestCase {
  
  var app: XCUIApplication?

  override func setUpWithError() throws {
    continueAfterFailure = false
    
    app = XCUIApplication()
    app?.launch()
  }
}

Et quand le test se termine, j’arrête également l’app.

  override func tearDownWithError() throws {
    app?.terminate()
  }
}

Vérifier l’état d’un toggle

Le premier test va vérifier l’état actuel d’un des toggle.

Pour cela je cré mon objet proxy et j’appelle la fonction checkToggleIsOff().

  func testPinInitialState() throws {
    let contentViewProxy = ContentViewTestProxy(app: app!)
    
    checkToggleIsOff(contentViewProxy!.pinToggle)
  }

Tester un changement d’état

Maintenant que je sais que l’état initial de ma vue est correct, je veux vérifier les interactions avec le toggle.

Avant de passer mon toggle de l’état off à on, il est préférable de vérifier qu’il est bien off. Pour cet exemple je change de méthode et j’utilise la fonction qui s’appuie sur les expectations.

Utiliser une expectation n’est pas utile, c’est juste une illustration de l’utilisation de cette fonction.

  func testTogglePinToOn() throws {
    let contentViewProxy = ContentViewTestProxy(app: app!)
    
    waitForSwitchToBeOff(contentViewProxy!.pinToggle, timeOut: 5)
    turnSwitchOn(contentViewProxy!.pinToggle)
  }

Souvenez-vous que turnSwitchOn() va également vérifier que l’action change bien l’état du toggle. Il n’est donc pas nécessaire d’ajouter un appel à la fonction waitForSwitchToBeOn().

Conclusion

Et voilà, nous avons terminé nos tests et nous disposons maintenant de fonctions simples pour implémenter des tests d’interface utilisateur de toggle.

Je vous ai également donné quelques conseils sur ma façon de mettre en œuvre des tests d’interface utilisateur. Des tests qui devraient ainsi pouvoir évoluer tout en étant d’une maintenance plus aisée.

Téléchargez le projet, et jouez avec. Bien sûr, tout commentaire est le bienvenu.

Ce code n’est pas encore réutilisable tel quel. Cette extension devrait faire partie d’un framework simple. J’y travaille, mais ce n’est pas encore prêt.

Code Source:

Le code source du project est entièrement disponible au téléchargement sur dépôt GitHub.