Comment tester un booléen optionnel

Benjamin Dumont
Just-Tech-IT
Published in
5 min readJun 11, 2020

La question peut paraître triviale car tout développeur sait utiliser un booléen. Cependant, notamment pour les booléens optionnels (prenant les valeurs true / false / null), différentes méthodes peuvent être utilisées à bon ou mauvais escient.

Pour le reste de l’article, je vais utiliser le langage Swift.

Exemple utilisé

Nous allons utiliser l’exemple suivant:

Une personne peut indiquer s’il elle aime le Swift ou le Kotlin (true), si elle est neutre (nil / null) ou si elle déteste ces langages (false).

Le but de l’exercice est d’afficher “Swift is wonderful” et/ou “Kotlin is wonderful” quand la valeur du booléen est true.

NB: un if ne peut traiter un booléen optionnel en Swift, il faut donc le rendre obligatoire d’une certaine façon.

Le cas classique

L’avantage de cette méthode c’est qu’elle est claire: on vérifie que le booléen est non null puis on regarde s’il est true.

Le gros inconvénient vient du “!” qui indique: “je sais qu’il est non null”. Si par mégarde, le développeur oublie la 1ère condition ou qu’il la retire, il y a un fort risque de crash de l’application.

Amélioration du cas classique

Dans le cas présent, plus aucun risque d’avoir une valeur nulle. En effet, l’opérateur “??” indique que si c’est null, on prend la valeur false (et donc on ne respecte pas la condition).

NB: Vous pouvez aussi remplacer la “,” de la condition par “&&” mais Swiftformat (formatter de code Swift) préfère la méthode affichée.

Cas classique avec assignation de la valeur

Ici, nous assignons la valeur de person3.likeSwift dans likeSwift. Si likeSwift est null, on ne rentre pas dans la condition du let, sinon on regarde si likeSwift est à true. Cette méthode est utile si vous avez besoin de la valeur de likeSwift à d’autres endroits; sinon il est préférable d’éviter de créer des variables inutiles.

La méthode concise

Ici, on va vérifier que la valeur est bien égale à true. Toute valeur différente (false ou null) nous sortira du if.

Bien que concise, cette méthode pose différents problèmes que j’aborde dans la section suivante.

Les inconvénients de la forme concise

  1. L’adaptation aux langages avec optionnels

Pour un développeur venant d’un langage ne contenant pas d’optionnel, il est étonnant de vérifier qu’une condition est égale à true (puisque on peut omettre le “== true”).

Par exemple, en Objective-C (langage compatible avec Swift), un booléen ne peut être null (et sa valeur de base est false). Les codes suivants ont donc exactement la même signification:

De ce fait, le 2ème code est assimilé à du mauvais code.

2. La perte de la notion d’optionnel

Il n’est pas forcément évident, à 1ère lecture, d’identifier ce qui est optionnel de ce qui ne l’est pas.

Dans ce cas de figure, on est obligé de regarder la déclaration pour savoir s’il est les booléens sont optionnels ou obligatoires (même si pour le 2ème “if”, on devrait retirer le “== true”). De ce fait, ça ne facilite pas la relecture du code. Tandis que le code suivant est bien plus explicite (le compilateur nous empêchant de mettre l’opérateur “??” pour des variables obligatoires via un Warning).

3. Changement de type

Dernier cas de figure: vous décidez à postériori de rendre le booléen obligatoire. Envisageons d’avoir initialement le code suivant:

Puis passons likeSwift en booléen obligatoire:

Le compilateur détecte qu’il y a du code inutile uniquement dans le 2nd “if” et vous invite donc à effectuer des modifications en indiquant clairement ce qui pose problème.

Pour le 1er “if”, il n’y a aucun problème de remonté alors que le code deviens obsolète puisqu’il n’y a plus de notion d’optionnel.

Les tests unitaires

  1. La méthode concise

Sur ce test, plusieurs problèmes apparaissent:

// 1: Si le test est en erreur, on ne connaît pas la raison immédiatement (personTest1 est null? likeSwift est null? likeSwift est false?). De plus on risque d’avoir des faux positifs/négatifs: En effet, on veut tester la valeur de likeSwift et non identifier si personTest1 est null.

// 2: Ici, si le test est en erreur, le compilateur nous donne la raison en donnant la valeur de chaque champ présent dans le XCTAssertEqual. Par contre, dans le cas où le champ gauche est null, impossible de déterminer si c’est personTest1 qui est null ou likeSwift. (A noter que cet Assert est un doublon de l’ Assert précédent)

//3: De la même façon, si le test est en succès, on ne peut déterminer s’il est en succès car personTest1 est null (faux positif) ou si c’est likeKotlin qui est null (ce qui est attendu)

2. Traiter les optionnels à part

Dans ce cas de figure, nous traitons en 1er lieu le cas d’une personne null. De ce fait, on maîtrise mieux le contenu des Asserts qui suivent et on peut se permettre de forcer le non-optionnel avec le “!”. L’inconvénient ici c’est que l’on risque le crash des tests (ça reste moins grave que dans l’application) et que tous les Asserts soient joués même si le 1er Assert est en erreur (donc possibilité de faux positifs/négatifs + délais de la phase de tests inutilement plus longue).

3. Traiter les optionnels à part en douceur

Ici, plus de risque de crash mais il reste 2 problèmes mineures:

  • Si un développeur supprime le 1er Assert, on se retrouve dans le 1er cas
  • Des Asserts joués inutilement si le 1er Assert est en erreur

4. Traiter les optionnels à part de manière optimisée

Avec XCTUnwrap, on sait que le result est personTest4 et est non null. Dans le cas contraire, le test serait en erreur et aucun autre Assert ne serait joué.

Conclusion

Sur un exemple aussi trivial, il peut y avoir différentes méthodes de développements. Cette analyse permet donc de dégager les bénéfices et inconvénients de chaque méthode. De manière générale, voici les règles que j’essaie de m’appliquer chaque jour:

  • Rendre le code lisible au premier coup d’oeil. C’est d’autant plus important quand votre équipe fonctionne avec des Pull Requests car cela permet de comprendre le code sans avoir besoin de le récupérer en local.
  • Tester le plus unitairement possible. En effet, selon moi, le résultat d’un Assert doit traiter le moins de cas possible, quitte à multiplier les Asserts. Cela permet entre autres de s’assurer du traitement du plus de cas possibles et améliore grandement l’analyse quand un test est en erreur.
  • Optimiser le code en gardant en tête les 2 règles précédentes, par exemple en anticipant les faux positifs/négatifs et évitant de jouer des Asserts qui n’ont plus de sens.

--

--