Aller au contenu
Home » Blog » Tests unitaires : produire du code facilement testable

Tests unitaires : produire du code facilement testable

Préambule

Le ministère pour ses nouveaux développements prône le recours à des techniques de programmation modernes et efficaces, qui ont fait leurs preuves. L’orientation du ministère met l’accent sur l’implémentation des tests unitaires. Ceux-ci permettent de vérifier le bon fonctionnement d’une portion de code (unité) et d’assurer la non-régression.

Ce guide donne des orientations et des pièges à éviter pour écrire du code de qualité,  facilement testable.

  1. Séparer les responsabilités

Une classe qui a plusieurs responsabilités est difficile à déboguer, à tester et à maintenir, car ses responsabilités deviennent couplées. Les modifications qui seront apportées à l’une des responsabilités pourront porter atteinte à la capacité de la classe de remplir les autres.

Pour identifier qu’une classe à des responsabilités multiples, essayez de résumer ce que cette dernière fait. Le « Et » dans cette description est un indicateur que celle-ci en fait probablement trop. Il existe plusieurs autres indicateurs, dont la difficulté à trouver un nom à la classe, un nombre important de méthodes, etc.

Le principe de la responsabilité unique permet une diminution de la complexité du code, une augmentation de la lisibilité, une meilleure cohésion, mais aussi une souplesse dans la mise en place des tests unitaires.

  • L’opérateur New (instanciations directes)

Il est assez fréquent lors de l’écriture du code, de mélanger l’opérateur « New » avec la logique du code. Procéder ainsi rend le code difficilement testable, en plus de créer une forte dépendance entre les classes.  

En effet, les tests unitaires se font sur des fragments de code isolés. À ce moment, vous ne serez plus en mesure d’isoler votre code comme il se doit pour effectuer correctement votre test unitaire.

Que ce soit dans un constructeur ou lors de la déclaration d’un champ, le mot clé « new » doit être évité. Il est préférable de passer vos objets en paramètres et effectuer des assignations dans le constructeur.

  • Appliquer la loi de Demeter (Demandez les choses. N’allez pas les chercher vous-même)

La loi de Demeter repose sur le principe qu’une classe ne doit communiquer qu’avec ses collaborateurs directs. Elle ne doit pas passer par un collaborateur pour faire appel à un autre. Ainsi, un objet A peut requérir un service (appeler une méthode) d’un objet B, mais A ne peut pas utiliser B pour accéder à un troisième objet et requérir ses services. Faire cela signifierait que A à une connaissance plus grande que nécessaire de la structure interne de B. Au lieu de procéder ainsi, B pourrait être modifié si nécessaire pour que A puisse faire appel direct à B.  Si la loi de Demeter est appliquée, seul B connait sa propre structure interne.

Concrètement, prenons les lignes de code suivantes :

public class MaClass
{
    private CategoriesService _categoriesService;
   
    public void AjouterCategorie(Categorie categorie)
    {
        _categoriesService.ListeCategories.AjouterCategorie(categorie);
    }
}

Selon cette loi, il faudrait plutôt que CategoriesService dispose d’une méthode pour ajouter une catégorie. Ainsi, MaClass ne saura pas comment sont stockées les catégories :

public class MaClass
{
    private CategoriesService _categoriesService;
   
    public void AjouterCategorie(Categorie categorie)
    {
        _categoriesService.AjouterCategorie(categorie);
    }
}
  • Éviter la logique dans le constructeur

Les tests unitaires doivent être en mesure de tester toute la logique implémentée dans une classe. Le constructeur de par sa définition, n’a pas besoin d’être testé.

Par ailleurs, pour chaque test unitaire que vous écrivez pour une classe, vous serez amené à instancier dans un premier temps le constructeur et la logique définie dans ce dernier devra d’abord être exécutée avec succès. Cela peut avoir un impact non négligeable sur les performances. Les mêmes conséquences seront également observées dans tous les autres tests qui auront besoin de cette classe lors de l’instanciation de leur graphe d’objets.

 Il est donc recommandé d’éviter de mettre toute logique dans le constructeur. En principe, vous ne devez rien faire d’autre que de l’assignation des champs dans le constructeur.

public class MaClass
{
    private CategoriesService _categoriesService;

    public MaClass(CategoriesService categorieService)
    {
        _categoriesService = categorieService;
    }
}
  • Éviter le recours aux Singletons

Le patron Singleton est l’un des patrons de conception les plus connus et simples à mettre en place.  Il s’agit également de l’un des plus mal utilisés. Le Singleton sert couramment à implémenter un contexte global de l’application, ce qui rend à la fois difficiles les tests unitaires et le débogage.

Concrètement, à l’exécution des tests unitaires, l’état du Singleton au premier appel sera indéniablement lié aux autres tests.  Pourtant, les tests unitaires doivent s’exécuter indépendamment et leur résultat ne doit pas être conditionné par un contexte.

Par ailleurs, les objets qui utilisent les Signgletons sont fortement couplés à la manière dont le Singleton est implémenté et la façon dont il est instancié. Il n’est pas possible de tester un objet qui dépend d’un Singleton sans tester le Singleton.

  • Éviter les méthodes statiques

De manière générale, les appels statiques sont des mauvaises pratiques, car ils induisent notamment un fort couplage entre les classes et rendent difficile la mise en place des tests unitaires.

En effet, pour toutes les méthodes qui font appel à une méthode statique, il sera quasiment impossible d’isoler convenablement ces dernières afin de mettre en place des tests unitaires.

  • Favoriser les compositions aux héritages

Pour produire du code de qualité et facilement maintenable, il est primordial de limiter le recours à l’héritage uniquement  aux besoins de polymorphisme.

 Il est assez courant pour les développeurs d’utiliser l’héritage comme moyen de réutilisation du code (méthodes) qui a été défini dans une autre classe. C’est une mauvaise pratique.  Cela entraine un couplage fort avec la classe mère, de la fragilité et des tests unitaires difficiles à mettre en place, moins performants et difficiles à maintenir.

Pour la réutilisation du code, il faut privilégier la composition.

  • Factoriser quand la complexité d’une méthode augmente

Plus un code est simple, plus il est facile à tester unitairement. La difficulté à mettre en place des tests unitaires augmente avec la complexité du code. Car, il faudra beaucoup plus d’effort pour écrire des tests permettant de parcourir toutes les lignes de code.

Des If/Else imbriqués, des commentaires pour expliquer la logique sont des signes qu’une méthode est trop chargée.

Face aux structures conditionnelles (switch, if imbriqués, etc.) vous devez favoriser le polymorphisme.

  • Dépendre des abstractions (injection des dépendances)

L’injection de dépendances prône le découplage entre les composants.  En effet, un composant B doit dépendre d’une abstraction (une interface) d’un composant A. Ainsi, le composant B n’a pas besoin de se préoccupé de comment le composant A est implémenté. De ce fait, l’implantation de A ou les changements qui pourront être apportés à ce dernier vont moins impacter le composant B.

Par ailleurs, cela simplifie grandement la mise en place des tests unitaires. Il est désormais plus aisé d’isoler le code à tester en fournissant des simulacres des dépendances utilisées.

  • Ne pas mélanger les objets valeurs avec les objets métiers

Les objets valeurs sont des entités, avec uniquement des accesseurs et des mutateurs. Un objet valeur n’implémente pas d’interface, pas de comportement externe et est facile à instancier. Ce dernier ne sera jamais substitué.

Les objets métiers par contre reposent sur des interfaces, appellent des collaborateurs et implémentent des comportements. Ils doivent être faciles à substituer. 

Un objet valeur ne doit jamais utiliser un objet métier. En revanche, un objet métier doit utiliser un objet valeur.

Les objets valeurs sont faciles à utiliser dans les tests unitaires, car ils peuvent être simplement créés à la volée et des assertions peuvent être effectuées sur leur état. Par contre, les objets métiers sont plus difficiles à utiliser dans les tests unitaires, car leur état n’est pas clair. C’est pourquoi des simulacres de ces derniers sont utilisés dans les tests.

Mélanger les deux pour créer un objet hybride ne peut qu’apporter de la complexité lors de l’écriture des tests unitaires.

Étiquettes:

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *