Bewise

Nous développons... votre avance

Ecrire des tests unitaires génériques

SLF
17/12/2007 - Benoît Laut
Télécharger la version Word

1 Introduction

Vous avez surement déjà entendu parler des tests unitaires, du débat constant entre leur utilité et la soit disant « perte de temps » pour le développeur. Il est vrai que bien souvent, écrire un test unitaire peut s’avérer long, même si aujourd’hui ce n’est pas les outils qu’il manque avec Visual Studio 2005 et maintenant Visual Studio 2008. Dans cet article nous n’entrerons pas dans le débat concernant l’utilité des tests unitaires, mais nous allons étudier un cas de test excessivement récurrent et voir comment celui-ci peut être rendu générique.

Pour aborder cet article sereinement il faut connaître les principes de base des tests unitaires, avoir des notions sur les mécanismes de reflection, ainsi que l’utilisation des génériques.

2 Le cas de test

2.1 Présentation par l’exemple

Prenons un exemple de test très simple. Vous avez développé une application qui possède une couche d’accès aux données et qui permet d’ajouter, de modifier, de supprimer un client. Vous avez besoin de tester la couche d’accès aux données et notamment le bon fonctionnement de la récupération d’enregistrements en base de données, de la modification, ou encore de l’insertion.

De quelle manière va-t-on procéder ?

Insertion d’un Customer en base de données, puis récupération du Customer à partir de son identifiant et enfin comparaison des 2 objets. De la même manière, modification de l’objet Customer et enregistrement en base de données, puis récupération du Customer à partir de son identifiant et enfin comparaison des 2 objets. On aurait donc quelque chose du genre :

image

De la même manière, prenons l’exemple d’une fonctionnalité d’import/export. L’utilisateur à la possibilité d’exporter des objets de l’application dans un format propriétaire que vous avez développé. Il a également la possibilité d’importer un fichier au format propriétaire. Cette action à pour effet de reconstruire un graphe d’objets à partir du fichier.

Le test de cette fonctionnalité aurait pour objectif de tester que les objets exportés (les objets de référence) sont identiques aux objets importés (les objets à tester).

De quelle manière procéderions-nous ?

Initialisation d’une liste d’objets, exportation de ces objets, importation des objets, et enfin pour chaque objet on ferait une comparaison de chaque propriété à travers le graphe d’objets. On aurait quelque chose du genre :

image

2.2 La problématique

Que peut-on retirer de ces exemples ?

Dans ces scénarios, la tache la plus contraignante est la comparaison des objets résultants de la fonctionnalité testée avec les objets de référence. En d’autre terme, nous écrivons un tas de ligne rien que pour comparer 2 objets de même type. De cette manière, le développeur passe du temps à écrire les assertions, ce qui peut engendrer des erreurs dans le code du test (ce serait le comble !) et donc au final on a perdu de vue l’intérêt premier de l’écriture de notre test, à savoir la fonctionnalité d’import/export. C’est la raison principale pour laquelle les développeurs n’aiment pas écrire des tests : c’est lourd à écrire.

Voyons les possibilités qui s’offrent à nous pour comparer 2 graphes d’objets avec le framework :

  • Tout d’abord celle que nous venons de voir, qui est de comparer à la mano chaque propriété de l’objet : la plus fastidieuse.
  • Si notre objet possède une surcharge de l’opérateur == et qui effectue l’opération que l’on désire, ou encore s’il implémente l’interface IComparer ou IComparable, qui compare toutes les propriétés comme nous le voulons alors ça résout le problème. Mais ne rêvons pas trop, il est rare d’avoir implémenté ce genre de chose pour tous nos objets.
  • Une autre possibilité, celle que nous allons mettre en œuvre dans la suite de cet article, qui est d’utiliser la puissance de la reflection pour écrire une méthode générique et réutilisable pour tous les tests de comparaison d’objets.

3 Présentation de la solution

Nous savons que les mécanismes de reflection sont couteux en performances. Cependant, dans les tests unitaires nous ne recherchons pas la performance, mais la fiabilité, la robustesse et la facilité à les écrire. Les mécanismes de reflection appliqués aux tests unitaires vont permettre de répondre à ces différents critères.

Par reflection, il nous suffit de parcourir les propriétés de l’objet de référence, de récupérer les valeurs associées et de stocker l’ensemble dans un dictionnaire. Il ne faut pas oublier de faire un traitement récursif dans le cas où la propriété est un type complexe (par exemple une commande possède une liste de ligne de commande). On fait la même chose pour l’objet résultant de la fonctionnalité testée. A ce moment là, nous nous retrouvons avec 2 dictionnaires, avec potentiellement les mêmes clés. Il ne reste plus qu’à comparer le contenu des 2 dictionnaires.

4 Implémentation de la solution

Reprenons l’exemple du test de l’import/export. Et voyons dans un premier de quelle manière nous voudrions écrire notre test, avec le moins d’effort possible :

image

En voyant ceci on remarque tout de suite la simplicité d’écriture du test. En effet, toute la complexité de validation du test a été déportée dans la classe TestHelper, de manière générique, afin d’être réutilisable pour n’importe quelle comparaison d’objet.

Dans un premier temps, voyons le contenu de la méthode Compare<T>(T reference, T aTester) :

image

Cette méthode récupère dans 2 dictionnaires les valeurs des propriétés de chacun des objets passés en paramètres. La présence du compteur permet d’être certain d’avoir une clé unique par élément ajouté dans les dictionnaires (les clés des dictionnaires sont obtenues en concaténant le nom de la propriété et du compteur). Une fois que les 2 dictionnaires sont constitués, il suffit de les comparer grâce à la méthode CompareDictionnary<K,V> décrite ci-dessous :

image

Cette méthode vérifie que tous les éléments présents dans le dictionnaire de référence sont présents dans le 2ème dictionnaire et que les valeurs sont identiques. La méthode concatène toutes les différences et retourne ce résultat, ce qui nous permettra de l’afficher dans le rapport d’erreurs du test.

Nous allons maintenant regarder le contenu de la méthode GetPropertiesValues(object o) car c’est ici que tout se passe :

image

Tout d’abord nous ne traitons pas l’objet si c’est une liste ou si ce n’est pas un type complexe. En effet, d’une part si l’objet à comparer est de type entier ou string, alors pas besoin de faire appel à ce mécanisme. D’autre part, si l’objet à comparer est une liste, alors il suffit de faire appel au mécanisme pour chaque élément de la liste.

Une fois que l’on est certain de ne pas être dans l’un de ces cas, alors on récupère la liste des propriétés et nous la parcourons. Si la propriété est un type valeur ou string, alors on peut ajouter dans le dictionnaire la valeur de la propriété. Si c’est un type complexe, on commence par récupérer la valeur. Ensuite plusieurs cas se présentent :

  • Soit la valeur est une liste, et dans ce cas, pour chaque élément on ajoute sa valeur dans le dictionnaire si c’est un type valeur ou string, ou alors on fait un appel récursif à la méthode et nous ajoutons dans le dictionnaire les éléments résultants de l’appel récursif grâce à la méthode AddRange.
  • Dans le cas contraire, nous faisons tout simplement un appel récursif avec la valeur de la propriété et là aussi nous ajoutons dans le dictionnaire les éléments résultants de l’appel récursif grâce à la méthode AddRange.
  • Pour information, il faudrait également traiter le cas où la propriété est de type IDictionary.

Voici le code de la méthode IsComplexType(Type t) :

image

La méthode AddPropertyValue(PropertyInfo prop, object val, Dictionary<string, string> dico) ci-dessous permet d’ajouter une clé unique dans le dictionnaire, composée du nom de la propriété et du compteur, et pour cette clé on ajoute la valeur de la propriété sous forme de chaine de caractères :

image

La méthode AddRange reprend le même principe que la méthode AddRange des types List<T>. Elle ajoute dans un dictionnaire les éléments d’un autre dictionnaire :

image

Il ne reste plus qu’une chose à définir. Dans la méthode GetPropertiesValues, pour chaque propriété nous faisons appel à la méthode MatchProperty en lui passant l’objet PropertyInfo courant. Cette méthode permet de savoir si pour une propriété donnée nous voulons ou non qu’elle soit traitée.

  • La première raison est la suivante : les propriétés pour lesquelles il n’existe pas d’accesseur get ne peuvent pas être traitées.
  • La seconde raison : bien souvent, lorsqu’on écrit un test et que nous comparons 2 objets, nous ne comparons pas certaines propriétés. Par exemple dans notre cas, nous ne voulons pas tester l’égalité des identifiants. Pour un test de base de données, insertion, mise à jour… nous ne voudrions pas tester l’égalité des dates de modification des objets par exemple. Donc cette méthode doit être développée selon les besoins du test.

image

Dans le code que nous avons écrit, l’appel à la méthode MatchProperty est statique et appellera toujours cette même implémentation. On pourrait se dire que pour chaque test que l’on écrit, la condition codée dans la méthode MatchProperty est différente. Nous pourrions très facilement y remédier en utilisant un délégué. Il suffirait d’implémenter, pour un test donné, la méthode MatchProperty adéquate et d’ajouter un paramètre de type delegate qui pointerait sur la méthode MatchProperty précédemment développée.

5 Conclusion

En quelques lignes de codes, nous venons donc de créer une méthode qui va simplifier l’écriture de beaucoup de tests. Ceci permet au développeur de se recentrer sur la problématique de base qui est la fonctionnalité à tester et non l’écriture du code du test.

Dans mon prochain article, nous reprendrons ce code que nous porterons sur le framework 3.5. Le but sera de découvrir quelques nouveautés de C# 3 que nous mettrons en pratique en refactorant le maximum de code de cette solution.

> Tous les articles

Commentaires

aucun commentaire
Page 1/1
   
Connexion
  • Accueil
  • Plan du site
  • Contact
Bewise TV, Blog technique, Webcasts...

Votre carrière et nous

  • Nos offres
  • Votre candidature
Ignorer les liens de navigation > Accueil > Nos Métiers > Solutions Langages et Framework > Détail Article
Ignorer les liens de navigation
Nous
Nos Métiers
Vous Former
Nos Evènements
Nos Références
Nos Activités
Nos Certifications
Nos Chiffres
Le Groupe
Nos Partenaires
On Parle de Nous
Nous contacter
Votre Carrière et Nous
Défiler vers le haut
Défiler vers le bas
Administration, Système et Communication
Architecture, Méthodes, Industrialisation
Décisionnel et Gestion des Données
Nouvelles Interfaces Utilisateurs
Portail et Travail Collaboratif
Solutions Langages et Framework
Solutions Web Avancées
Défiler vers le haut
Défiler vers le bas
Nos cours
Le Planning
Offres promotionnelles
Défiler vers le haut
Défiler vers le bas
Bewise Day Conference 2011
Nos Expresso
Défiler vers le haut
Défiler vers le bas
  • Infos légales
  • Lettre du Regional Director
  • Revue de presse