Skip to content

Blog


Comment accélérer le développement et les tests de SwiftUI en utilisant PreviewSnapshots

18 janvier 2023

|
John Flanagan

John Flanagan

L'une des grandes caractéristiques du développement dans SwiftUI est la suivante Aperçus de Xcode qui permettent une itération rapide de l'interface utilisateur en rendant les changements de code en temps quasi réel avec le code SwiftUI. Chez DoorDash, nous utilisons beaucoup les prévisualisations de Xcode ainsi que l'application Test de l'instantané bibliothèque de Sans point pour s'assurer que les écrans ressemblent à ce que nous prévoyons lors de leur développement et qu'ils ne subissent pas de changements inattendus au fil du temps. SnapshotTesting peut être utilisé pour capturer une image rendue d'un écran de View et créer un XCTest si la nouvelle image ne correspond pas à l'image de référence sur le disque. Les prévisualisations de Xcode en combinaison avec SnapshotTesting peuvent être utilisées pour fournir des itérations rapides tout en s'assurant que les vues continuent à ressembler à ce qu'elles sont censées être sans crainte de changements inattendus. 

Le défi d'utiliser les prévisualisations Xcode et SnapshotTesting ensemble est qu'il peut en résulter un grand nombre de modèles et de duplication de code entre les prévisualisations et les tests. Pour résoudre ce problème, DoorDash Engineering a développé PreviewSnapshots, un outil open-source de snapshot de prévisualisation qui peut être utilisé pour partager facilement des configurations entre les prévisualisations Xcode et les tests de snapshot. Dans cet article, nous allons approfondir ce sujet en fournissant d'abord quelques informations sur le fonctionnement des prévisualisations Xcode et SnapshotTesting, puis en expliquant comment utiliser le nouvel outil open-source avec des exemples illustratifs sur la façon d'éliminer la duplication du code en partageant les configurations de vue entre les prévisualisations et les snapshots.

Comment fonctionnent les aperçus de Xcode

Aperçus de Xcode permettent aux développeurs de renvoyer une ou plusieurs versions d'un View d'un PreviewProvider et Xcode rendra une version vivante de l'élément View ainsi que le code de mise en œuvre.

Depuis Xcode 14, les vues avec plusieurs aperçus sont présentées sous forme d'onglets sélectionnables en haut du canevas d'aperçu, comme illustré dans la figure 1.

Figure 1 : L'éditeur Xcode montre le code de la vue SwiftUI pour afficher un message simple, ainsi que le canevas de prévisualisation Xcode qui rend deux versions de cette vue. L'une avec un message court et l'autre avec un message long.
Figure 1 : L'éditeur Xcode montre le code de la vue SwiftUI pour afficher un message simple, ainsi que le canevas de prévisualisation Xcode qui rend deux versions de cette vue. L'une avec un message court et l'autre avec un message long.

Comment fonctionne SnapshotTesting

La bibliothèque SnapshotTesting permet aux développeurs d'écrire des assertions de test sur l'apparence de leurs vues. En affirmant qu'une vue correspond aux images de référence sur le disque, les développeurs peuvent être sûrs que les vues ne changent pas de manière inattendue au fil du temps.

L'exemple de code de la figure 2 compare les versions courte et longue de MessageView avec les images de référence stockées sur le disque en tant que testSnapshots.1 et testSnapshots.2 respectivement. Les clichés ont été enregistrés à l'origine par SnapshotTesting et automatiquement nommée d'après la fonction de test avec la position de l'assertion dans la fonction.

Figure 2 : L'éditeur Xcode montrant le code SwiftUI View utilisant PreviewSnapshots pour générer des prévisualisations Xcode pour quatre états d'entrée différents ainsi que le canvas de prévisualisation Xcode rendant la vue en utilisant chacun de ces états.
Figure 2 : L'éditeur Xcode montrant le code SwiftUI View utilisant PreviewSnapshots pour générer des prévisualisations Xcode pour quatre états d'entrée différents ainsi que le canvas de prévisualisation Xcode rendant la vue en utilisant chacun de ces états.

Le problème de l'utilisation conjointe de Xcode Previews et de SnapshotTesting

Il y a beaucoup de points communs entre le code utilisé pour les prévisualisations Xcode et pour la création de tests instantanés. Cette similitude peut entraîner une duplication du code et des efforts supplémentaires pour les développeurs lorsqu'ils essaient d'adopter les deux technologies. Idéalement, les développeurs pourraient écrire du code pour prévisualiser une vue dans une variété de configurations et ensuite réutiliser ce code pour les tests instantanés de la vue dans ces mêmes configurations.

Présentation de PreviewSnapshots

C'est en résolvant ce problème de duplication du code que PreviewSnapshots peut être utile. PreviewSnapshots permet aux développeurs de créer un ensemble unique d'états de vue pour les prévisualisations Xcode et de créer des cas de test instantanés pour chacun des états avec une seule assertion de test. Nous allons voir ci-dessous comment cela fonctionne avec un exemple simple. 

Utilisation de PreviewSnapshots pour une vue simple

Supposons que nous ayons une vue qui recueille une liste de noms et les affiche d'une manière intéressante.

Traditionnellement, nous voulons créer un aperçu pour quelques états intéressants de la vue. Peut-être vide, un seul nom, une courte liste de noms et une longue liste de noms.

struct NameList_Previews: PreviewProvider {
  static var previews: some View {
    NameList(names: [])
      .previewDisplayName("Empty")
      .previewLayout(.sizeThatFits)

    NameList(names: ["Alice"])
      .previewDisplayName("Single Name")
      .previewLayout(.sizeThatFits)

    NameList(names: ["Alice", "Bob", "Charlie"])
      .previewDisplayName("Short List")
      .previewLayout(.sizeThatFits)

    NameList(names: [
      "Alice",
      "Bob",
      "Charlie",
      "David",
      "Erin",
      //...
    ])
    .previewDisplayName("Long List")
    .previewLayout(.sizeThatFits)
  }
}

Ensuite, nous écrirons un code très similaire pour les tests instantanés.

final class NameList_SnapshotTests: XCTestCase {
  func test_snapshotEmpty() {
    let view = NameList(names: [])
    assertSnapshot(matching: view, as: .image)
  }

  func test_snapshotSingleName() {
    let view = NameList(names: ["Alice"])
    assertSnapshot(matching: view, as: .image)
  }

  func test_snapshotShortList() {
    let view = NameList(names: ["Alice", "Bob", "Charlie"])
    assertSnapshot(matching: view, as: .image)
  }

  func test_snapshotLongList() {
    let view = NameList(names: [
      "Alice",
      "Bob",
      "Charlie",
      "David",
      "Erin",
      //...
    ])
    assertSnapshot(matching: view, as: .image)
  }
}

La longue liste de noms pourrait éventuellement être partagée entre les prévisualisations et les tests instantanés à l'aide d'une propriété statique, mais il est impossible d'éviter d'écrire manuellement un test instantané individuel pour chaque état prévisualisé.

PreviewSnapshots permet aux développeurs de définir une collection unique de configurations intéressantes, puis de les réutiliser de manière triviale entre les prévisualisations et les tests d'instantanés.

Voici à quoi ressemble un aperçu de Xcode en utilisant PreviewSnapshots : 

struct NameList_Previews: PreviewProvider {
  static var previews: some View {
    snapshots.previews.previewLayout(.sizeThatFits)
  }

  static var snapshots: PreviewSnapshots<[String]> {
    PreviewSnapshots(
      configurations: [
        .init(name: "Empty", state: []),
        .init(name: "Single Name", state: ["Alice"]),
        .init(name: "Short List", state: ["Alice", "Bob", "Charlie"]),
        .init(name: "Long List", state: [
          "Alice",
          "Bob",
          "Charlie",
          "David",
          "Erin",
          //...
        ]),
      ],
      configure: { names in NameList(names: names) }
    )
  }
}

Pour créer une collection de PreviewSnapshots, nous construisons une instance de PreviewSnapshots avec un tableau de configurations ainsi qu'un fichier configure pour définir la vue d'une configuration donnée. Une configuration se compose d'un nom et d'une instance de State qui sera utilisé pour configurer la vue. Dans ce cas, le type d'état est [String] pour le tableau de noms.

Pour générer les aperçus, nous renvoyons snapshots.previews de la norme previews comme l'illustre la figure 3. snapshots.previews générera un aperçu correctement nommé pour chaque configuration de l'option PreviewSnapshots.

Figure 3 : L'éditeur Xcode montre le code SwiftUI View utilisant PreviewSnapshots pour générer des aperçus Xcode pour quatre états d'entrée différents ainsi qu'un canevas d'aperçu Xcode rendant la vue en utilisant chacun de ces états.
Figure 3 : L'éditeur Xcode montre le code SwiftUI View utilisant PreviewSnapshots pour générer des aperçus Xcode pour quatre états d'entrée différents ainsi qu'un canevas d'aperçu Xcode rendant la vue en utilisant chacun de ces états.

Pour une petite vue facile à construire, PreviewSnapshots fournit une structure supplémentaire mais ne fait pas grand-chose pour réduire les lignes de code dans les aperçus. Le principal avantage pour les petites vues se présente lorsqu'il est temps d'écrire des tests d'instantanés pour la vue.

final class NameList_SnapshotTests: XCTestCase {
  func test_snapshot() {
    NameList_Previews.snapshots.assertSnapshots()
  }
}

Cette seule assertion testera chaque configuration dans les PreviewSnapshots. La figure 4 montre l'exemple de code avec les images de référence dans Xcode. En outre, si de nouvelles configurations sont ajoutées dans les aperçus, elles seront automatiquement testées sans modification du code de test.

Figure 4 : Test unitaire Xcode utilisant PreviewSnapshots pour tester quatre états d'entrée différents définis ci-dessus avec un seul appel à `assertSnapshots`.
Figure 4 : Test unitaire Xcode utilisant PreviewSnapshots pour tester quatre états d'entrée différents définis ci-dessus avec un seul appel à assertSnapshots

Pour les points de vue plus complexes comportant de nombreux arguments, l'avantage est encore plus grand.

Utilisation de PreviewSnapshots pour une vue plus complexe

Dans notre deuxième exemple, nous examinons un FormView qui prend plusieurs Bindingun message d'erreur facultatif et une fermeture d'action comme arguments de son initialisateur. Cela démontrera les avantages accrus de PreviewSnapshots à mesure que la complexité de la construction de la vue augmente.

struct FormView: View {
  init(
    firstName: Binding<String>,
    lastName: Binding<String>,
    email: Binding<String>,
    errorMessage: String?,
    submitTapped: @escaping () -> Void
  ) { ... }

  // ...
}

Depuis PreviewSnapshots est générique sur l'état d'entrée, nous pouvons regrouper les différents paramètres d'entrée dans une petite structure d'aide à passer dans la fonction configure et n'ont plus qu'à faire le travail de construction d'un FormView une fois. Pour plus de commodité PreviewSnapshots fournit une NamedPreviewState pour simplifier la construction des configurations d'entrée en regroupant le nom de la prévisualisation avec l'état de la prévisualisation.

struct FormView_Previews: PreviewProvider {
  static var previews: some View {
    snapshots.previews
  }

  static var snapshots: PreviewSnapshots<PreviewState> {
    PreviewSnapshots(
      states: [
        .init(name: "Empty"),
        .init(
          name: "Filled",
          firstName: "John", lastName: "Doe", email: "[email protected]"
        ),
        .init(
          name: "Error",
          firstName: "John", lastName: "Doe", errorMessage: "Email Address is required"
        ),
      ],
      configure: { state in
        NavigationView {
          FormView(
            firstName: .constant(state.firstName),
            lastName: .constant(state.lastName),
            email: .constant(state.email),
            errorMessage: state.errorMessage,
            submitTapped: {}
          )
        }
      }
    )
  }
  
  struct PreviewState: NamedPreviewState {
    let name: String
    var firstName: String = ""
    var lastName: String = ""
    var email: String = ""
    var errorMessage: String?
  }
}

Dans l'exemple de code, nous avons créé un PreviewState qui est conforme à la norme NamedPreviewState pour contenir le nom de l'aperçu ainsi que le prénom, le nom de famille, l'adresse électronique et le message d'erreur facultatif pour construire la vue. Ensuite, dans le champ configure nous créons une instance unique de FormView en fonction de l'état de configuration transmis. En renvoyant snapshots.preview de PreviewProvider.previewsPreviewSnapshots va boucler sur les états d'entrée et construire une prévisualisation Xcode correctement nommée pour chaque état, comme le montre la Figure 5. 

Figure 5 : L'éditeur Xcode montre le code SwiftUI View utilisant PreviewSnapshots pour générer des aperçus Xcode pour trois états d'entrée différents ainsi qu'un canevas d'aperçu Xcode rendant la vue en utilisant chacun de ces états.
Figure 5 : L'éditeur Xcode montre le code SwiftUI View utilisant PreviewSnapshots pour générer des aperçus Xcode pour trois états d'entrée différents ainsi qu'un canevas d'aperçu Xcode rendant la vue en utilisant chacun de ces états.

Une fois que nous avons défini un ensemble de PreviewSnapshots pour les aperçus, nous pouvons à nouveau créer un ensemble de tests d'aperçus avec une seule assertion de test unitaire.

final class FormView_SnapshotTests: XCTestCase {
  func test_snapshot() {
    FormView_Previews.snapshots.assertSnapshots()
  }
}

Comme dans l'exemple plus simple ci-dessus, ce scénario de test comparera chacun des états de prévisualisation définis dans la section FormView_Previews.snapshots par rapport à l'image de référence enregistrée sur le disque et génère un échec du test si les images ne correspondent pas aux attentes.

Conclusion

Cet article a abordé certains des avantages de l'utilisation des prévisualisations Xcode et du SnapshotTesting lors du développement avec SwiftUI. Il a également démontré certains des points de douleur et la duplication du code qui peuvent résulter de l'utilisation de ces deux technologies ensemble et comment PreviewSnapshots permet aux développeurs de gagner du temps en réutilisant l'effort qu'ils ont mis dans l'écriture des prévisualisations Xcode pour les tests d'instantanés. 

Les instructions pour intégrer PreviewSnapshots dans votre projet, ainsi qu'un exemple d'application utilisant PreviewSnapshots, sont disponibles sur GitHub.

A propos de l'auteur

  • John Flanagan

    John Flanagan est ingénieur logiciel chez DoorDash, depuis septembre 2021, au sein de l'équipe iOS Infrastructure, se concentrant sur les technologies qui permettent aux ingénieurs iOS de DoorDash de proposer des fonctionnalités plus rapidement et de manière plus fiable.

Emplois connexes

Localisation
San Francisco, CA ; Mountain View, CA ; New York, NY ; Seattle, WA
Département
Ingénierie
Localisation
San Francisco, CA ; Sunnyvale, CA
Département
Ingénierie
Localisation
San Francisco, CA ; Sunnyvale, CA ; Seattle, WA
Département
Ingénierie
Localisation
Pune, Inde
Département
Ingénierie
Localisation
San Francisco, CA ; Seattle, WA ; Sunnyvale, CA
Département
Ingénierie