Tester c’est douter ! Tout le monde connaît cette phrase, elle est souvent employée sur un ton comique entre les QAs et les développeurs pour se « troller » les uns les autres, il n’en reste pas moins qu’elle met en exergue une caractéristique importante de l’état d’esprit d’un QA qui automatiserait un test métier ou d’un développeur qui coderait un test unitaire ; Le doute ! Douter de l’efficacité et/ou de la performance d’un code permet d’apporter de la robustesse à ses assertions et donc d’améliorer la qualité du test. Dans cet article, nous allons voir comment faire du doute une orientation d’implémentation pour nos tests.
Le Property-Based Testing, Quesako ?
Le property-based-testing est une approche qui nous vient du monde de la programmation fonctionnelle, rendue d’abord populaire par la librairie QuickCheck auprès des développeurs Haskell, elle s’est vue propagée à une audience plus large ces dernières années. Cette approche s’articule autour de la notion de « Propriété » qui peut dans certains cas être assimilée à la notion d’« Aspect ». Une propriété est une caractéristique importante de notre SUT (System Under Test), elle ne tient généralement pas compte d’un détail précis et n’est pas vérifiable par un contrôle statique.
Principe
L’idée derrière cette approche est subtile ; En agissant au niveau des propriétés, nous n’allons pas vérifier que le SUT est bien fonctionnel, mais plutôt le contraire, nous allons essayer de le casser par tous les moyens, on sera gagnants à tous les tableaux :
- Si on arrive à casser le SUT alors on prouve qu’il est perfectible et on pourra dès lors engager des travaux d’amélioration.
- Si on n’y arrive pas alors on est content car notre SUT est bien robuste et qu’il résiste bien à nos attaques.
Je reprends l’exemple de l’assurance habitation de « Romeu MOURA » que je trouve très pertinent et que vous pouvez retrouver dans l’enregistrement de la session « Domain Invariants & Property-Based Testing for the masses » à DDD Europe 2017 : Lien Youtube
Souscrire à une assurance habitation ce n’est rien de plus qu’un pari que vous entretenez avec votre assureur ; Vous payez vos cotisations tous les mois en pariant qu’il y aura un feu chez vous alors votre assureur lui s’engage à vous couvrir en pariant qu’il ne se passera rien. Dans ce pari, vous êtes heureux de perdre tous les mois, mais vous continuez de parier tout de même. C’est exactement la même chose avec le Property-Based-Testing (PBT), on parie contre un système en essayant de le casser et on est content de perdre car cela veut dire que notre code est robuste ! Maintenant que c’est plus clair, nous allons passer à l’exemple.
Use case
Nous allons choisir un exemple simple que vous connaissez déjà pour faciliter la prise de repère, il s’agit du KATA FizzBuzz ! On a des nombres en entrée et pour chacun d'eux :
- Si le nombre est divisible par 3 : on écrit Fizz.
- Si le nombre est divisible par 5 : on écrit Buzz.
- Si le nombre est divisible par 3 et par 5 : on écrit FizzBuzz.
- Sinon on écrit le nombre en entrée
Illustration
Nous allons coder en C# sur Visual Studio Code pour cet exemple, mais vous pouvez le faire en employant le langage de votre choix.
Commençons par créer le projet de test en exécutant cette commande sur un Terminal Windows (ou sur Visual Studio Code) :
dotnet new xUnit
Ensuite nous allons ajouter les librairies FsCheck pour déclarer nos propriétés et les tester dans la foulée ;
Il s’agit d’un Framework créé à la base pour F# mais qui fonctionne très bien pour tous les langages .NET.
Vous trouverez toutes les informations utiles sur FsCheck en visitant leur Repo Github : https://fscheck.github.io/FsCheck/
Pour ajouter la core lib FsCheck à votre projet, mettez-vous à la racine de votre dossier projet et exécuter la commande suivante :
dotnet add package FsCheck
Ensuite nous allons ajouter une deuxième librairie sympa qui nous permettra de piloter le framework xUnit par FsCheck ;
Restez à la racine et exécutez la commande suivante :
dotnet add package FsCheck.Xunit
Vous allez voir apparaître 2 nouveaux packages nuget dans votre fichier projet (CSPROJ)
Voilà, notre projet est configuré, nous allons passer à l’implémentation du code.
D’abord le SUT
Créez une nouvelle classe FizzBuzz dans votre projet et implémentez la comme suit :
public class FizzBuzz { public string Compute(int input) { if(input % 3 == 0 && input % 5 == 0) return "FizzBuzz"; if(input % 3 == 0) return "Fizz"; if(input % 5 == 0) return "Buzz"; return input.ToString(); } }
Ensuite le Test
Voilà le code :
using FsCheck; using FsCheck.Xunit; namespace PBT { public class UnitTest { [Property] public Property CheckFizz(int x) { return (new FizzBuzz().Compute(x) == "Fizz").When(x % 3 == 0 && x % 5 != 0); } [Property] public Property CheckBuzz(int x) { return (new FizzBuzz().Compute(x) == "Buzz").When(x % 3 != 0 && x % 5 == 0); } [Property] public Property CheckFizzBuzz(int x) { return (new FizzBuzz().Compute(x) == "FizzBuzz").When(x % 3 == 0 && x % 5 == 0); } [Property] public Property CheckNumber(int x) { return (new FizzBuzz().Compute(x) == x.ToString ()).When(x % 3 != 0 && x % 5 != 0); } } }
Vous remarquerez que les annotations pour déclarer les tests ont changé, effectivement grâce au package FsCheck.Xunit, nous pouvons directement déclarer les propriétés en remplaçant les annotations standard xUnit [Fact] par [Property] C’est plutôt sympa car cela met davantage en avant la notion de « Property » sur chaque test, en gros un test = une property !
Également, cela nous permet une écriture des tests au format simple « Should, When », je trouve ça pas mal.
Bon, il est temps d’exécuter tout ça et de voir ce que ça fait ;
Exécution
Clic-droit dans le corps du test « CheckFizz » et choisissez « Run Tests in Context » :
Vous devrez alors obtenir l’output suivant :
Sympa ! FsCheck a pu vérifier 100 fois que l’algo fonctionnait bien pour Fizz.
Je vous illustre son fonctionnement par ce diagramme d’activité ;
Maintenant nous allons exécuter l’ensemble des tests, mettez-vous à la racine de votre projet et exécutez la commande suivante :
dotnet test
Vous devriez obtenir le résultat suivant :
Nous avons 3 tests OK et un test KO, en réalité il s’agit du 3ème test « CheckFizzBuzz », nous allons l’exécuter en solo et voir ce qui cloche.
Même principe, Clic-droit dans le corps du test « CheckFizzBuzz » et choisissez « Run Tests in Context », vous devrez alors obtenir ce message d’erreur ;
Cela veut dire que FsCheck n’est pas content 😊, il n’arrive pas à atteindre les 100 matchs car il estime que filtre qu’on lui a attribué est trop restrictif ;
Il boucle alors jusqu’à ce qu’il considère que le délai est écoulé et il nous répond gentiment qu’il refuse de poursuivre.
Nous allons donc l’aider un peu en lui fournissant un générateur plus malin capable d’avoir les 100 matchs rapidement.
Nous allons rajouter la classe suivante dans le code :
public static class FizzBuzzGenerator { public static Arbitrary<int> Generate() { return Arb.Default.Int32().Filter(x => x % 3 == 0 && x % 5 == 0); } }
Et référencer ce « générateur » dans l’annotation du test CheckFizzBuzz par celle-ci
public static class FizzBuzzGenerator { [Property(Arbitrary = new[] { typeof(FizzBuzzGenerator) })] public Property CheckFizzBuzz(int x) { return (new FizzBuzz().Compute(x) == "FizzBuzz").When(x % 3 == 0 && x % 5 == 0); } }
Et relancer l’ensemble des tests, vous devrez maintenant voir tout VERT.
Interprétation
En nous mettant dans une approche PBT, nous avons implémenté ces 4 tests différemment que ce que nous aurions pu faire naturellement. Effectivement, durant cet exercice nous avons écrit des tests qui n’avaient pas pour vocation de valider des critères d’acceptation, mais plutôt l’inverse ; nous avons vraiment essayé de casser le SUT par l’emploi de la Randomisation. Globalement, nous parions à chaque exécution des tests que nous réussirons à prouver que le SUT ne répond pas aux exigences et nous sommes heureux de perdre à chaque fois 😊.
Conclusion
Nous avons vu dans cet article comment raisonner dans une approche PBT avec un exemple simple et un Framework sympa (FsCheck). Je vous invite à consulter la documentation officielle de la librairie où vous découvrirez des déclinaisons intéressantes :
- Les Lazy Properties
- Les conditionnal Properties
- Les Trivial Properties
- Les Classified Properties
- Les Collected Properties
- … et plein d’autres qui vous ouvriraient l’appétit pour aller bien loin avec cette approche.
Vous pouvez télécharger le code source de l'exercice par ici ! #CassezLesCodesAvecLePBT 😊