Comment tester le code de projection dans Swift

Tout ce que vous devez savoir sur le test des fonctions de projection avec XCTest et sur la préservation et la robustesse du code de test.

Combien de fois avez-vous dû prendre en charge un projet comportant des tests unitaires, mais ils étaient difficiles à saisir, échouaient désespérément ou la cible de test ne serait même pas construite?

Il est essentiel de maintenir le code de test unitaire robuste et maintenable, afin de ne pas le laisser abandonné et ignoré au fil du temps.

Chez Storytel, nous essayons de rendre nos tests unitaires courts et lisibles. En raison de sa nature, le code de test a tendance à être long et répétitif. Il est donc important de le garder propre. Travailler avec des tests ne devient pas fastidieux à mesure que le projet se développe.

Le code qui jette peut être difficile à tester de temps en temps, et le code de test résultant peut être moche. Dans cet article, je vais plonger dans différents scénarios et comment les résoudre de manière agréable et robuste.

XCTest est un framework puissant. Dans Xcode 8.3, Apple a introduit quelques nouvelles fonctions XCTAssert en plus d'une vingtaine de fonctions existantes. Bien que les fonctionnalités qu'ils fournissent permettent de faire la plupart des choses que le développeur souhaite, certaines choses nécessitent toujours du code standard plutôt que des fonctionnalités fournies nativement.

Voyons quelques cas et comment nous les résolvons.

Comparer les résultats des fonctions de lancer

Celui-ci est facile. Toutes les fonctions XCTAssert existantes prennent déjà des arguments. Si le résultat est Equatable, une ligne comme celle-ci fera l'affaire:

XCTAssertEqual (essayez x.calculateValue (), attenduValue)

Si CalculateValue () renvoie une erreur, le test avec échec renvoie le message «XCTAssertEqual a échoué: erreur renvoyée…». Si l'appel n'a pas été lancé et que deux valeurs ne sont pas égales, le test avec échec portant le message «XCTAssertEqual a échoué: a n'est pas égal à b», où «a» et «b» sont respectivement des descriptions d'arguments gauche et droit, produites par String (décrivant :).
En termes simples, toutes les fonctions de vérification de valeur XCTAssert * vérifient qu’un appel ne génère aucune erreur et renvoie le résultat attendu. Très pratique.

Fonctions avec des types de retour non-Equatable

Souvent, XCTAssertEqual et ses frères et sœurs qui vérifient les valeurs ne suffisent pas.

Si une fonction ne renvoie pas de valeur ou si elle peut être ignorée dans le contexte d'un scénario de test, nous pouvons utiliser la nouvelle fonction XCTAssertNoThrow qui est devenue disponible dans Xcode 8.3:

XCTAssertNoThrow (essayez x.doSomething ())

Il est intéressant de noter qu’avant la publication de Xcode 8.3, nous avions une fonction personnalisée qui portait exactement la même signature et nous n’avions pas à changer de code, sauf à supprimer notre implémentation personnalisée.

Un autre cas courant est celui où le type retourné n'est pas Equatable, ou si nous voulons vérifier seulement certaines propriétés du résultat retourné.
Même si le type est Equatable, l’échec du cas de test d’égalité est presque inutile lorsque la description de l’objet est très longue: il est difficile de savoir quel champ a une valeur incorrecte:

Dans nos projets, nous avons beaucoup de fonctions qui produisent des types complexes qui ne sont pas conformes à Equatable. Un exemple courant est celui des objets de modèle de données: nous les exposons sous forme de protocoles masquant l’implémentation interne et nous ne voulons pas qu’ils soient équitables. Chaque type de modèle possède un initialiseur de projection utilisant un dictionnaire.

À un moment donné, nous avons réalisé que nos tests unitaires semblaient hideux. Ils avaient un code répétitif, des options sans signification et lorsque ces tests échouaient, tout était rouge. Pour aggraver encore les choses, il y avait beaucoup de copier-coller. Le code ressemblait à ceci:

XCTAssertNoThrow (essayez BookObject (dictionary: sampleDictionary))
laisser book = essayer? BookObject (dictionary: sampleDictionary)
XCTAssertEqual (livre? .Name, "...")
XCTAssertEqual (livre?. Description, "...")
XCTAssertEqual (book? .Rating, 5)
...
// "meilleure" version:
XCTAssertNoThrow (essayez BookObject (dictionary: sampleDictionary))
XCTAssertEqual (essayez BookObject (dictionary: sampleDictionary) .name, "...")
XCTAssertEqual (essayez BookObject (dictionary: sampleDictionary) .description, "...")
XCTAssertEqual (essayez BookObject (dictionary: sampleDictionary) .rating, 5)
...
Ce n’est pas neuf échecs, c’est un…

La solution s'est avérée simple. L'astuce consistait à extraire un résultat produit par le premier autoclosure de XCTAssertNoThrow, puis à exécuter une fermeture de validation supplémentaire sur celui-ci, mais uniquement dans le cas où il y aurait un résultat.

public func XCTAssertNoThrow  (_ expression: @autoclosure () lève -> T, _ message: String = "", fichier: StaticString = #file, ligne: UInt = #line, également validateResult: (T) -> Void ) {
    func executeAndAssignResult (_ expression: @autoclosure () jette -> T, vers: inout T?), affiche de nouveau {
        to = essayer expression ()
    }
    var résultat: T?
    XCTAssertNoThrow (essayez executeAndAssignResult (expression, to: & result), message, fichier: fichier, ligne: ligne)
    si r = resultat {
        validateResult (r)
    }
}

Maintenant, les mêmes tests semblaient beaucoup plus raisonnables: lisibles, fortement typés et produisant des messages digestibles.

Lancer des erreurs spécifiques

Dans certains cas, nous voulons tester le contraire: une fonction génère une erreur. Ceci est souvent nécessaire pour tester la désérialisation du modèle.
Nous pouvions déjà le faire avec la fonction existante XCTAssertThrowsError, bien que si nous voulions vérifier qu’une erreur spécifique avait été émise, nous devions fournir une fermeture pour évaluer l’erreur émise.

En regardant le type de vérifications que nous avions habituellement là-bas, nous avons constaté qu’il n’en existait que deux: comparer l’erreur renvoyée à l’erreur attendue ou simplement en vérifier le type. Nous avons donc créé deux fonctions pratiques pour transformer ces tests en une seule ligne:

public func XCTAssertThrowsError  (expression _: @autoclosure () lève -> T, expectError: E, _ message: String = "", fichier: StaticString = #file, ligne: UInt = #ligne ) {
    XCTAssertThrowsError (try expression (), message, fichier: fichier, ligne: ligne, {(erreur) dans
        XCTAssertNotNil (erreur comme? E, "\ (erreur) n'est pas \ (E.self)", fichier: fichier, ligne: ligne)
        XCTAssertEqual (erreur comme? E, expectError, fichier: fichier, ligne: ligne)
    })
}
public func XCTAssertThrowsError  (expression _: @autoclosure () lève -> T, attenduErrorType: E.Type, _ message: String = "", fichier: StaticString = #file, ligne: UInt = #line ) {
    XCTAssertThrowsError (try expression (), message, fichier: fichier, ligne: ligne, {(erreur) dans
        XCTAssertNotNil (erreur comme? E, "\ (erreur) n'est pas \ (E.self)", fichier: fichier, ligne: ligne)
    })
}

Même si ça ne jette pas…

La puissance des fonctions de projection peut être utilisée pour écrire des tests plus robustes pour d'autres scénarios également, où l'égalité régulière n'est pas applicable.

Pensez à avoir une énumération qui ne peut pas être rendue équitable - par exemple, si les valeurs associées de ses cas ne sont pas Équables.
Au lieu de faire basculer les scénarios de test, nous écrivons des fonctions «d'assistance» pures qui génèrent des erreurs significatives.

Un exemple courant est une énumération de résultat:

enum Résultat {
    succès de la requête ([String: Any])
    cas d'échec (Erreur)
}

Si nous testions une fonction qui renvoie directement Result, nous devrions basculer la valeur renvoyée et appeler XCTFail dans les cas erronés. Nous copierions le commutateur pour chaque cas de test et la mise à jour des tests pour les nouveaux cas d'énum serait un cauchemar.

Au lieu de cela, nous pouvons créer des fonctions de lancement d’aides pour gérer l’enum en un seul endroit:

XCTAssert (essayez result.assertIsSuccess (assertValue: {(valeur: [String: Any])) dans
    XCTAssertEqual (value.count, 10)
}))
XCTAssert (essayez result.assertIsFailure (assertError: {(valeur: Error) dans
    XCTAssertEquals (valeur, MyError.case)
}))
// MARK: Aides
extension privée Résultat {
    Private enum Error: Swift.Error, CustomStringConvertible {
        var message: String
        Description de la variable: String {return message}
    }
    func assertIsSuccess (assertValue: (([String: Any]) lève -> vide)? = nil) lève -> Bool {
        changer soi-même {
        case .success (valeur let):
            essayez assertValue? (valeur)
            retourne vrai
        case .failure (_):
            throw Error (message: "Attendu .success, got. \ (self)")
        }
    }
    func assertIsFailure (assertError: ((Erreur) lève -> Vide)? = nil) lève -> Bool {
        changer soi-même {
        case .success (_):
            throw Error (message: "Erreur attendue, got. \ (auto)")
        case .failure (let error):
            essayez assertError? (valeur)
            retourne vrai
        }
    }
}

Ce type d’approche peut être utilisé dans divers scénarios, tels que la vérification gracieuse d’options (qui sont également des énums: troll :).

Une note sur la création de fonctions d'assertion personnalisées

Il y a peu de choses à garder à l'esprit lors de l'écriture de fonctions de test personnalisées.

  1. Il est recommandé d’ajouter des arguments de ligne et de fichier, en les transmettant jusqu’aux fonctions XCTAssert standard. De cette manière, les échecs de scénario de test sont signalés au moment où votre assertion personnalisée est appelée, et non dans le corps de la fonction elle-même.
  2. Il est bon d’ajouter un paramètre de message afin que l’appelant puisse fournir un contexte au test. C’est aussi bien de les utiliser lors de la rédaction de scénarios de test :)
  3. XCTFail (message :) fournit un moyen d’échouer inconditionnellement un test, ce qui peut être très utile, par exemple. en testant des domaines non équitables et en tombant dans un cas inattendu.

*

Le framework XCTest est devenu très puissant au cours de la dernière année et nous essayons de réutiliser autant que possible les fonctions existantes au lieu de reproduire leur comportement.

Il est intéressant de noter que, pour NSExceptions, l'infrastructure XCTest fournit une API plus riche, qui n'est malheureusement disponible que dans Objective-C: https://developer.apple.com/documentation/xctest/nsexception_assertions?language=objc

Une documentation complète pour toutes les fonctions d'assert peut être trouvée ici: https://developer.apple.com/documentation/xctest

A l'origine inspiré par ceci: http://ericasadun.com/2017/05/27/tests-that-dont-crash/

*

Mise à jour pour Xcode 9: aucune nouvelle API d’affirmation n’a encore été ajoutée. Nous espérons pouvoir prendre en charge NSExceptions dans la version Swift de XCTest!

*

Mise à jour pour Xcode 10.2: aucune nouvelle API d’affirmation n’a été ajoutée.

J'ai commencé à chercher des moyens de contribuer à la mise en œuvre open-source et créé un argumentaire sur les forums Swift. Voulez-vous m'aider avec ça? Contactez-moi ici ou sur Twitter, améliorons XCTest ensemble!

Merci d'avoir lu! Si vous avez aimé l'article, partagez-le en cliquant sur le bouton Partager situé en dessous de l'article - plus de personnes en bénéficieront.

Vous pouvez également me suivre sur Twitter, où j'écris principalement sur le développement iOS.

Veux-tu travailler avec moi? Storytel recrute - postulez ici