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.
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.
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
.
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.
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 Binding
un 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.previews
PreviewSnapshots 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.
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.