Paris, Bordeaux, Niort, Nantes, Rennes, Toulouse, Aix, Nice
tests-unitaires

Tests Unitaires en .NET

Introduction

Lorsqu’on parle de tests en programmation, on peut parler d’une multitude de choses : de simples clics à la souris sur tous les boutons d’une interface graphique pour voir si ça plante, des tests End To End automatisés, de tests d’intégration ou encore de tests unitaires. Et ce sont de ces derniers dont j’aimerais vous parler dans cet article.

Présentation des tests unitaires

Avant de commencer à parler en détails des tests unitaires, on peut se demander ce qu’est un test.

Un test est là pour vérifier et valider la bonne réalisation d’un comportement. Automatisé ou manuel, il doit être reproductible et prédictible.

Selon la nature de l’objet testé, on pourra alors parler de tests unitaires.

Le test unitaire est un test en isolation d’une fonctionnalité du programme, que l’on peut tester en tant qu’unité individuelle.

C’est un test n’ayant pas d’effet de bord persistant, n’utilisant pas d’effet de bord ou de résultat d’un autre test et n’impactant aucun autre test. Il doit parcourir du code écrit par le développeur.

La fin de cette définition nous fait réfléchir sur la cible de nos tests unitaires.

En effet, tester c’est bien, mais tester quoi ?

Une des choses à retenir c’est qu’il faut tester uniquement le code et les fonctionnalités réalisées par nos soins. Il est inutile, par exemple, de tester une librairie qui a des millions de téléchargements à son actif, d’autres s’en sont déjà occupés pour vous !

Appliquez-vous à tester la logique métier, c’est l’essentiel, ne faites pas du « taux de coverage » de vos tests une religion.

Pourquoi ça peut coincer ?

On se concentre donc sur notre code, on essaye de faire nos premiers tests unitaires, et on bloque, on galère … Pourquoi donc ?

Tout simplement parce que tous les codes ne sont pas facilement testables et peuvent vous obliger à refactoriser pour mettre en place vos tests.

C’est pour cela qu’il est important de penser aux tests pendant la phase de développement : créez des interfaces, gérez vos dépendances en utilisant l’injection de dépendance, gardez en tête le « Single Responsibility Principle », une fonctionnalité qui ne fait qu’une seule chose sera bien plus facile à tester.

Et si ma fonctionnalité a besoin du résultat d’une autre, je fais comment ?

Dans ce cas-là, on va simuler !

Il existe des librairies permettant d’utiliser ce que l’on appelle des « mocks ».

Ce sont des objets que l’on va créer, sur la base de l’interface qu’implémente l’objet réel (ou de la classe abstraite dont il hérite), pour se substituer aux éléments extérieurs dont la fonctionnalité a besoin.

On va pouvoir les configurer pour qu’ils aient un comportement spécifique quand ils sont appelés ou même vérifier le nombre de fois qu’une méthode attendue a été appelée.

Présentation d’un test concret

Pour réaliser nos tests sur un projet .Net Core, on va d’abord devoir créer un projet de test et lui référencer notre projet testé.

Puis il sera temps de créer une classe de test qui contiendra nos méthodes de test pour cette classe.

Je passe ici sur toute cette création, la documentation Microsoft explique cela très bien, je vais juste vous montrer ici à quoi ressemble une classe et une méthode de test et décrire un peu ce que l’on y voit.

Il existe plusieurs Framework de Test : MSTest, NUnit et XUnit, pour l’exemple ci-dessous, j’utilise MSTest et le Framework de Mock : Moq.

Beaucoup de choses en une image, décomposons ce test :

D’abord le nom, il peut paraitre à rallonge mais il est important que le nom de la méthode de test décrive au mieux le comportement attendu et ce qui est testé.

Ici, le test vérifie que si l’on essaye d’ajouter un évènement déjà existant, la fonctionnalité renvoie un état invalide.

Ensuite, un test unitaire se fait normalement en 3 phases : Arrange, Act et Assert.

Dans la partie Arrange, nous allons créer les mocks et les fake data dont notre test a besoin pour fonctionner.

Ici 5 choses sont réalisées :

  • Les deux premières variables ligne 204 et 205 sont des Mocks de service dont a besoin ma classe testée pour s’instancier.
  • La troisième variable ligne 206 est un objet réel métier, initialisé avec de fausses données pour servir de paramètre à la méthode.
  • La quatrième chose réalisée en  ligne 218 est une configuration du mock « fakeCalendarEventDapper », nous indiquons ici que si lors du test, la méthode « IsCalendarEventExist » est appelée avec n’importe quel « CalendarEventDAO » en paramètre, alors cette méthode retournera true.

C’est ce qui nous permet d’isoler notre test de fonctionnalité, sans cela il sera impossible de savoir si le test échoue car notre logique métier est mauvaise ou simplement car cette méthode externe est elle-même défaillante.

Grace à cette simulation de résultat des dépendances externes, on teste uniquement la logique métier de notre fonctionnalité.

  • Enfin nous allons instancier notre classe à tester à l’aide des mocks crées pour cela.

Dans la partie Act nous allons simplement appeler la méthode à tester de notre objet, en lui passant des paramètres.

Dans la partie Assert, nous allons nous assurer que les résultats attendus sont observés.

Dans cet exemple nous allons nous assurer de deux choses :

  • D’abord que la méthode testée appelle bien dans son déroulement la méthode « IsCalendarEventExist ».
  • Puis que la méthode « Add » du service mocké « fakeCalendarEventDapper » n’est jamais appelée.
  • Puis que le « result » a bien sa propriété IsValid à false.

L’écriture du test est terminée ! Il nous reste à le lancer via l’explorateur de test pour nous assurer que le comportement attendu est là.

Imaginons maintenant que quelqu’un touche à notre méthode et que l’appel à la méthode Add se fasse même si l’évènement existe, en rejouant le test après sa modification, le développeur verra immédiatement que la méthode n’agit plus comme attendu :

En multipliant les tests sur cette méthode (renvoie false si « Add » renvoi false, renvoie false avec un paramètre fake invalide …) nous allons pouvoir nous assurer que cette fonctionnalité reste conforme même en cas de modification.

Cependant, lorsque je dis « en multipliant les tests », il ne faut pas aller dans l’excès, ne testez pas toutes les possibilités existantes pour une fonctionnalité.

Si par exemple, une méthode prend un entier en paramètre, contentez-vous de tester sa réaction au bord et pourquoi pas aux limites (MaxValue, MinValue, -1,0,1) sinon vous partirez pour une infinité de tests et ne reviendrez jamais…

Conclusion

Les tests unitaires sont les tests les plus nombreux à effectuer et prennent du temps, mais ils sont indispensables pour veiller au bon fonctionnement de chaque pièce de votre « œuvre » et permettent de se rendre compte rapidement d’une régression lors d’un ajout ou d’une modification.

Le fait de penser à la manière dont on code pour rendre son code testable a entrainé l’apparition d’une autre méthode de développement logiciel, basée sur la réalisation de tests avant chaque ligne de code, le Test-Driven Development (TDD), ou développements pilotés par les tests.

CET ARTICLE A ÉTÉ RÉDIGÉ PAR :
Michaël Naxara

MICHAEL NAXARA
INGÉNIEUR LOGICIEL JUNIOR
ALLTECH BORDEA
UX
Sources :

Documentation Microsoft : https://docs.microsoft.com/fr-fr/visualstudio/test/unit-test-basics?view=vs-2019

Vidéo Microsoft Developer sur les tests unitaires : https://www.youtube.com/watch?v=NTO2P4K0CGo

Commit Strip : http://www.commitstrip.com/fr/?

Remerciements : Farouk DRIF rencontré dans le cadre de ma formation