FluentAssertion est une library dotnet qui fournit un ensemble très complet de méthodes d’extension qui vous permettent de spécifier plus naturellement le résultat attendu d’un test unitaire.

À travers mes expériences j’ai pu remarquer une difficulté redondante qui sévi dans l’esprit des développeurs; je veux bien sûr parler des tests unitaires 👿. Pour ma part, j’ai été formaté à l’écriture de tests, j’ai de façon inconsciente intégré les tests unitaires à ma méthodologie de travail. Il m’a fallu un peu de temps avant de me rendre compte de cette facilité que j’ai acquise, et encore plus pour comprendre comment je trick mon cerveau à fonctionner de la sorte. J’ai ainsi constaté 2 choses :

  • 🥱 Dans la tête d’un développeur, test unitaire sonne comme une barrière à franchir, un effort supplémentaire à fournir.
  • 🧘 De mon côté, un test unitaire, c’est un moment d’amusement et de relaxation, qui m’apporte une aide gratuite dans le développement.

On voit bien 2 idées en totale opposition, et j’ai pu analyser le contenu de ce gouffre qui nous sépare. J’ai chronologiquement constaté que :

  • J’ai tendance à produire une couverture de tests supérieure à la moyenne 💪. Il m’arrive d’ailleurs d’être le seul à produire des tests unitaires.
  • Je m’amuse avec ces tests unitaires, je ne fournis aucun effort alors que je gagne en couverture de tests.
  • C’est cocasse, à quel moment j’ai pu trouver un amusement dans ce monde de tests unitaires ? Autrement dit, quels sont les outils et méthodes que j’utilise pour me faciliter la vie et automatiquement écrire des tests unitaires ?

Pas d’inquiétude, je ne vais pas employer de termes qui font peur, ou vous demander d’apprendre des méthodologies, il n’y aura pas de TDD, BDD ou whatever chez moi. On cherche l’efficacité directe, que du bonus, pas de friction.
Posez-vous d’ailleurs la question de ce que le TDD apporte vraiment, ne répétez pas juste ce que l’on vous dit. Réfléchissez par vous-même, remettez en question vos acquis, toujours.

D’ailleurs, si le TDD était si incroyable, on verrait des tests unitaires dans les projets non ? Dans cet article on va considérer que le TDD c’est fait pour flex devant les commerciaux en entretien. C’est cadeau. 👀

Je vais répondre directement à cette dernière constatation de façon pragmatique, allons droit au but, voilà ce que sont les tests unitaires pour moi :

  • Découverte d’un outil / Débogage rapide avec un point d’entrée simple. Executer un test c’est plus pratique qu’un programme console.
  • Démystifier son propre code. Un test c’est rapide à écrire, rapide à executer, contrairement à l’entièreté de l’application.
  • ✅ Pas de documentation, pas de recherche, juste de l’intuition. Mon SUT (System Under Test) doit avoir pour résultat xxx. => On traitera ce point dans le présent article !
  • ✅ J’écris les tests quand j’ai envie. Avant, pendant, après, il n’y a pas de règle stricte. Je ne fais jamais de TDD.
  • Pendant l’écriture d’une fonctionnalité, je me demande si cette ligne que je suis en train d’écrire va se comporter d’une certaine façon => Pouf test unitaire !

Vous l’aurez compris en lisant le titre de l’article, nous allons parler de FluentAssertions !

TL;DR: Vous n’avez pas besoin d’un article. Installez le package FluentAssertions et le problème est plié. Merci de m’avoir lu, vous êtes désormais certifié unit test evangelist whatever or something.

Si vous restez jusqu’à la fin de l’article, j’ai prévu un petit bonus spécialement pour vous !

Entrée en matière

❔ Nota bene: J’utilise ici nUnit, mais si vous souhaitez utiliser xUnit, remplacez simplement l’attribut [Test] par [Fact]. Peu importe le framework de test, FluentAssertions est compatible. On veut tester, pas se poser de question sur le framework de tests.

Regardons ce code:

Copy to Clipboard

A part le clin d’œil à un artiste légendaire 🎤, notons ici qu’il y a 2 assertions. Pour chacune d’elle, et pour toutes les assertions que nous allons écrire, le point de départ est le résultat du SUT suivi de l’appel à Should(). Si vous avez l’habitude des assertions classiques, vous avez l’habitude de commencer par écrire Assert..

Qu’est-ce que l’on gagne ici ? Remarquez la cohérence entre le nom du test et l’assertion. On veut tester two plus two et donc on va commencer notre assertion par 2 + 2. Pour l’écriture du test, on va forcément partir de notre résultat sur lequel on veut effectuer l’assertion. Dans le cas des Assert, on va devoir se poser la question d’où placer ce résultat. On augmente la complexité cognitive dès le départ.

Second point que l’on peut observer avec cet exemple, le point d’entrée pour choisir la méthode d’assertion à appeler vient du retour de la méthode Should(). On va donc avoir une liste de choix basée sur l’objet que l’on teste. Dans le cas d’un int, on aura par exemple parmi les choix la méthode Be(int) comme utilisé dans cet exemple. On aura aussi par exemple BePositive(). Dans le cas d’un bool, on retrouve évidemment BeTrue(). Dans le cas des Asserts., impossible de déterminer ce que l’on teste. C’est au développeur de faire le tri, de trouver quelle méthode appeler.

Avec un exemple aussi simple que celui-ci, on voit déjà 2 points de frictions qui disparaissent. Vous voyez déjà l’usage incroyable que cela peut amener ? Vous voyez juste, on ne va jamais voir la documentation de FluentAssertions !

A retenir pour l’usage de cette library:

  • On part du résultat de notre SUT, de ce que l’on veut tester.
  • On appelle la méthode Should().
  • On se sert de l’autocomplétion.
  • La méthode Be() prend en paramètre la valeur attendue. A utiliser quand on attend un résultat exact.

Débloquer la vraie puissance des tests: Simple

Je veux vous partager 2 points cruciaux :

  1. (Encore) la facilité de placer des assertions sur n’importe quel objet.
  2. L’expérience de debugging et d’analyse.

Commençons par regarder ce code, qui je pense, est auto-décrit :

Copy to Clipboard

Nous l’avons vu par le passé, il est important pour un supermarché d’être équipé en pâtes 🍝. Je vous fait grâce de l’explication du code, ce serait une insulte à votre capacité à lire un texte simple. Je n’ai fait que montrer une petite partie des possibilités, voyez comme il est simple de trouver l’assertion qui nous convient.

⚠ Notez tout de même que j’appelle toujours Should() directement sur mon supermarket. Je ne fais pas de traitement sur mon système pour ensuite faire l’assertion. Pour reprendre une des assertions au dessus, voici la guideline :

Copy to Clipboard
Vous allez voir pourquoi cela est important dans la suite de cet article. Stay tuned ! 🏎️

 

Je vous propose un autre exemple rapide, avant de finir de vous convaincre :
Copy to Clipboard

Voilà ! Remarquez bien la cohérence entre le nom du test et l’assertion, c’est bluffant.

Voyons maintenant ce qu’il se passe quand un test échoue. Je vous spoil, l’expérience développeur est là, on est loin du flou laissé par le retour des Assert..

Copy to Clipboard

Ce qui nous donne :
Expected result to be "this is right", but "this is wrong" differs near "wro" (index 8).

Comparons avec un Assert.AreEqual(expected, result ); :

String lengths are both 13. Strings differ at index 8.
  Expected: "this is right"
  But was:  "this is wrong"
  -------------------^

Ici on remarque une différence fondamentale qu’il faudra garder en tête, FluentAssertion sait que ce qui est testé est la variable result. Si je renomme result en choucroute :

Copy to Clipboard

Expected choucroute to be "this is right", but "this is wrong" differs near "wro" (index 8).

De même, un test qui échoue sur la comparaison de collections donne un résultat assez naturel.

Copy to Clipboard

Notez la mention précise de la property.

Expected property resultCoordinates[1].X to be 1, but found 0.
Expected property resultCoordinates[1].Y to be 0, but found 1.

Le même code avec un nombre différent d’éléments cette fois ci.

Copy to Clipboard

L’indication est très précise à nouveau, on nous indique que la collection resultCoordinates devrait avoir 2 éléments. On a le droit à un output lisible des 2 collections.

Expected resultCoordinates to be a collection with 2 item(s), but 
{
    SF.Blog.FluentAssertionsTests.CollectionsTests+Coordinate
    {
        X = 0, 
        Y = 0
    }, 
    SF.Blog.FluentAssertionsTests.CollectionsTests+Coordinate
    {
        X = 0, 
        Y = 1
    }, 
    SF.Blog.FluentAssertionsTests.CollectionsTests+Coordinate
    {
        X = 0, 
        Y = 2
    }
}
contains 1 item(s) more than
{
    SF.Blog.FluentAssertionsTests.CollectionsTests+Coordinate
    {
        X = 0, 
        Y = 0
    }, 
    SF.Blog.FluentAssertionsTests.CollectionsTests+Coordinate
    {
        X = 1, 
        Y = 0
    }
}.

J’ai intentionnellement masqué une partie de l’output 👤, lors de la comparaison de collections, on a le droit à un rappel de toutes les options utilisées. La plupart du temps les paramètres par défaut sont amplement suffisants. S’il vous manque quelque chose, n’essayez pas de le faire vous-même, FluentAssertions a probablement déjà prévu le scénario pour vous.

Nous terminons ce chapitre avec un élément indispensable, mais tout aussi intuitif : Les exceptions. ❌

Pour l’exemple, voici une méthode qui throw une exception, avec une inner exception.

Copy to Clipboard

L’appel à la méthode Invoking prend en paramètre une Action dans cet exemple. Action sur laquelle on va s’attendre à lever une exception. Comme vous pouvez le voir, il est possible de spécifier précisément ce que l’on veut, autant le type que le message.

Copy to Clipboard

Pour ceux qui veulent garder une syntaxe de type AAA (Arrange Act Assert) vous pouvez très bien faire ceci :

Copy to Clipboard

Les Assertion Scopes, un outil bien pratique : Basique

Le sujet que l’on va aborder ici pour illustrer nos exemples tourne autour de Unicode. Cela commençait à devenir trop simple, amusons-nous un peu. L’emoji 😂 est stocké dans 2 char apparemment, mais faisons comme si l’on ne le savait pas. Vous pouvez voir 2 choses dans le test suivant.

Premièrement, on voit en commentaire que 2 assertions doivent échouer. Dans un test unitaire, l’exécution s’arrête à la première assertion qui n’est pas satisfaite. J’ai donc fait exprès de préparer 2 échecs.

Deuxièmement, vous le voyez venir, on a un AssertionScope. Il va tout simplement agréger toutes les exceptions et les retourner à la fin.

Copy to Clipboard

Voyez plutôt

Expected emote.Length to be 1, but found 2.
Expected emote with length 1, but found string "😂" with length 2.

Les plus malins auront remarqué que je n’ai pas respecté mes propres guidelines 😱😱😱. Ou presque.
Les 2 assertions qui échouent testent exactement la même chose. On voit qu’avec l’appel à HaveLength() donne beaucoup plus d’information.

J’entends que vous en voulez plus, alors voici un autre exemple un peu plus tordu. D’un côté, voyez que la bonne solution à l’Unicode est la normalization. Ensuite, pour FluentAssertions, il peut être parfois utile de donner une explication à l’assertion via l’argument (because). La string doit commencer par une miniscule et ne pas finir par un point.

Copy to Clipboard

L’output en vaut le détour.

Expected normalized to be "Å" with a length of 1 because the normalized is not represented as the same Unicode, but "Å" has a length of 2, differs near "Å" (index 0).
Expected normalized to be "Å" with a length of 1 because this is the 00c5 form, but "Å" has a length of 2, differs near "Å" (index 0).

Bref, vous l’aurez compris, utilisez un AssertionScope pour éviter de commenter / dé-commenter toutes vos assertions lors de vos phases de débogage. Je sais que vous le faites 😉.

Bonus : Nommer sa méthode de test sans effort

Vous avez tenu jusqu’ici, je vais vous récompenser avec une vraie technique de productivité. Comme je l’ai fait remarquer au début, tout cet article a pour but de vous donner des outils pour vous rendre la vie plus facile, plus amusante, mais en aucun cas rajouter de la complexité. Vous n’êtes pas sans savoir que le nommage est un concept considéré complexe dans le monde du développement logiciel. Nos yeux saignent tous les jours à force de lire du code mal nommé. Je ne peux pas vous aider à maîtriser l’anglais et vous éviter certaines horreurs de grammaire. Cependant, voici une technique qui fonctionne réellement, quant bien même des développeurs (peu regardant quant à leur propre progression ma fois 🤡) pourraient la railler.

C’est très simple, commencez votre test comme ceci :

Copy to Clipboard

Vous n’avez créé absolument aucune friction 🕊️. Si vous aviez peur de vous lancer dans l’écriture d’un test unitaire, essayez cela. Franchement c’est pas super effrayant, je pense que l’on peut tous le faire.

Ensuite, créez votre SUT tout naturellement. A ce stade-là, vous n’avez peut être pas la moindre idée de ce que vous allez tester.

Vous allez donc invoquer une méthode, pour changer l’état de votre SUT (ou pas). A ce niveau-là, laissez-vous guider par l’autocomplétion, vous allez voir l’inspiration arriver toute seule.

Finissez par prendre le résultat du point précédant, et commencez à taper Should(). et laissez-vous guider.

Franchement, n’est-ce pas trop simple ? Pas besoin de réfléchir. Et en prime, oh ! Vous avez le nom de votre test. Voyez plutôt.

Copy to Clipboard

Ça c’est de l’ingénierie ! 🖥️

Comme je suis bien sympathique, je vous donne un dernier tip. Je vous ai dit que le runner de tests est un très bon entry point, meilleur qu’un programme console. Si vous ne savez vraiment pas quoi tester, vous pouvez très bien commencer par ceci :

Copy to Clipboard

Conclusion

Les avantages de FluentAssertions :

  • Améliore la qualité du code.
  • Réduit la friction, aucune complexité cognitive.
  • Meilleur output lorsqu’une assertion échoue.
  • Aide à trouver le nom du test, ou inversement le nom du test aide à écrire l’assertion.
  • Versatile et compatible. Compatible avec tous les frameworks de tests et tout code existant.

Je ne vous liste pas les inconvénients, j’essaie de vous vendre un concept parfait à travers cet article. Plus nous serons nombreux à le connaître, plus il sera adopté naturellement dans les projets. Il n’y a aucune condition pour l’utiliser alors je vous invite dès maintenant à en parler avec votre équipe de développement et écrire vos nouveaux tests avec FluentAssertions. Pas la peine de migrer les anciens tests, encore que certains outils sont capable de le faire automatiquement (dans une certaine mesure).

Si ce sujet vous a intéressé, n’hésitez pas à me faire des retours. Si vous souhaitez échanger autour de ces sujets de code… fluent 😁 n’hésitez pas à entrer en contact. Dites-moi quelles sont vos techniques pour vous faciliter la vie avec les tests unitaires !

Vous en voulez plus ? Un autre sujet que je trouve très bafoué dans l’industrie est par exemple les messages de commit…

Liens

Ne ratez plus aucune actualité avec la newsletter mensuelle de SoftFluent

Newsletter SoftFluent