Architecture applicative minimum pour tester unitairement

L’un des points fondamentaux pour réaliser un test automatisé est de le rendre reproductible. L’un des critères pour qu’un test soit unitaire est qu’une seule méthode soit testée de façon isolée sans dépendre d’une base de données ou de tout autre système externe. Le moyen le plus efficace pour assurer ces deux caractéristiques est d’utiliser des mocks. Trop souvent, lorsqu’on prononce ces mots devant un client, des réactions de méfiance apparaissent : on a besoin de la base de données dans l’équipe, avec notre code ça n’est pas possible, c’est compliqué… L’objectif de ce billet est de montrer une architecture applicative minimale pour répondre à ce besoin.

Sur mon projet actuel la majorité de l’équipe .NET ne connaissait pas la problématique de tests unitaires. La majorité de leur code se présentait sous forme de méthodes statiques appelant au final des procédures stockées, cas somme tout assez fréquent dans les projets d’application de gestion. Le premier refactoring doit assurer un découpage en couche minimal : une classe métier MyService contenant le code à tester, une classe MyDAO contenant le minimum de code permettant d’appeler une procédure stockée car il ne sera pas testé unitairement. Voici quelques points simples à vérifier par MyDAO pour s’assurer d’un découpage effectif :

  • Chaque méthode de MyDAO prend les valeurs de ses paramètres (si possible des POCOs) les transforme en paramètres SQL. Elle appelle la procédure stockée et renvoie le résultat. Ce sont les seules responsabilités que ces méthodes sont autorisées à prendre.
  • Les seules structures conditionnelles doivent concerner des cas d’erreur ou du paramétrage SQL. Déporter toute structure if dans la classe MyService en créant au besoin deux méthodes séparées dans la classe MyDAO
  • .

Voici un exemple simplifié de la classe MyService qui sera testée :

public class MyService {
  public static Customer CreateCustomer(string name, string surname) {
    //Code métier
   MyDAO.createcustomer(name, surname, technicalReference);
   //Code métier
  }
}

Afin d’introduire les premiers tests unitaires sur la classe MyService, nous avons indiqué à l’équipe de suivre désormais les quelques règles suivantes:

  • Première règle : utiliser des singletons pour transformer les appels statiques en appel d’instance à moindre coût.
  • Deuxième règle : définir une interface pour chaque classe.
  • Troisième règle : placer une référence à MyDAO dans MyService et appeler les méthodes sur cette référence à la place des méthodes statiques.
  • Quatrième règle : dans le constructeur de mon service, initialiser la référence à MyDAO avec le singleton

    Voici le résultat sur notre exemple :

    public class MyService {
      private IMyDAO _MyDAO;
      
      public static readonly IMyService Instance = new MyService();
    
      private MyService() {
        _MyDAO = MyDAO.Instance();
      }
      
      public Customer CreateCustomer(string name, string surname) {
        //Code métier
       _MyDAO.Createcustomer(name, surname, technicalReference);
       //Code métier
      }
    }

    Ce premier changement a été suffisamment léger pour être facilement appréhendé par les développeurs. Maintenant voyons l’utilité qu’on l’on peut en tirer :

  • Cinquième règle : Ajouter un constructeur public prenant en argument IMyDAO.

Comme ceci :

public class MyService
    {
        private IMyDAO _MyDAO;

        public static readonly IMyService Instance = new MyService();

        private MyService()
        {
            _MyDAO = MyDAO.Instance();
        }

        /// <summary>
        /// Ce constructeur est uniquement dédié aux tests unitaires et n’est pas conçu pour être utilisé en temps normal
        /// Il pourra être accédé depuis les tests unitaires en ajoutant [assembly: InternalsVisibleTo("UnitTests")] sur l'assembly
        /// </summary>
        /// <param name="MyDAO"></param>
        internal MyService(IMyDAO MyDAO)
        {
            _MyDAO = MyDAO;
        }

        public Customer CreateCustomer(string name, string surname)
        {
            //Code métier
            _MyDAO.CreateCustomer(name, surname, technicalReference);
            //Code métier
        }
    }

Il est maintenant possible d’écrire un test unitaire de la façon suivante : entièrement mocké, entièrement isolé. L’environnement est parfaitement maîtrisé à chaque exécution du test.

//Mock
class DaoMock : IMyDAO {
	public Customer CreateCustomer(string name, string surname, int technicalReference) {
	  Assert.AreEqual("TestName", name);
	  Assert.AreEqual("TestSurname", surname);
	  return new Customer("TestName", "TestSurname");
	}
}

[Test]
public void TestCreateCustomerOK() {
  
  IMyDAO daoMock = new DaoMock();
  
  IMyService serviceToTest = new MyService(daoMock);
  Customer result = serviceToTest()
  Assert.AreEqual("TestName", result.Name);
  Assert.AreEqual("TestSurname", result.Surname);
}

Très rapidement dans ce projet, nous avons introduit le framework Rhino Mock pour diminuer la quantité de code nécessaire pour les mocks. Mais l’infrastructure sous-jacente étant déjà présente cela a permis d’appréhender le framework plus facilement. L’exemple suivant met en regard les nouveaux concepts introduits par rapport au code précédent :

[Test]
public void TestCreateCustomerOK() {
  
  MockRepository mockRepository = new MockRepository();
  IMyDAO daoMock = mockRepository.StrictMock<IMyDAO>();
  Customer sample = new Customer("TestName", "TestSurname");
  Expect.Call(daoMock.CreateCustomer("TestName", "TestSurname")).Return(sample);
  mockRepository.ReplayAll();
  
  IMyDAO daoMock = new DaoMock();
  
  IMyService serviceToTest = new MyService(daoMock);
  Customer result = serviceToTest()
  Assert.AreEqual("TestName", result.Name);
  Assert.AreEqual("TestSurname", result.Surname);
}

Cette solution est minimaliste : il ne s’agit pas d’inversion de contrôle car le lien vers l’implémentation est dispersé dans les différentes classes. Cependant, elle permet de réaliser le découplage minimal pour garantir des tests réellement unitaires tout en minimisant l’impact sur un code existant. Les classes appelant préalablement MonService.CreateCustomer(...) devront ainsi simplement appeler MonService.Instance.CreateCustomer(...).
Tous ces exemples ont été réalisés en C#/.NET, technologie de mon projet actuel. Notez cependant qu’ils sont transposables quasi en l’état pour un projet Java. Si un framework comme Spring, souvent cité dans le monde Java lorsqu’on réalise des tests unitaires, n’est pas un besoin immédiat, une telle architecture applicative peut être une alternative plus légère pour permettre l’introduction des premiers tests unitaires.
C’est donc une raison de plus pour commencer dès aujourd’hui à écrire des tests sur votre code!

11 commentaires sur “Architecture applicative minimum pour tester unitairement”

  • Et cette architecture minimum devrait l'être pour de nombreux projets...c'est dommage que l'on traîne aujourd'hui des cadavres dans des placards où le test a été oublié, et la charge de travail pour remettre le tout d'aplomb est tellement grande que ça en est décourageant. Merci pour ce billet !
  • Pas tout à faire d'accord sur le deuxième constructeur. S'il s'agit d'un singleton, il ne peut avoir de constructeur public. Pourquoi ne pas exposer un accesseur (setter) plutôt ?
  • @Eric Il est vrai que le problème de l'épurement de la dette technique sur les tests est très compliqué et très couteux. Un façon de commencer à le traiter est de faire "Un bug, Un test". Lorsqu'un bug est remonté, on commence par écrire un test qui le met en évidence et on corrige ensuite le code pour que le test passe. Petit à petit, le niveau de couverture va remonter.
  • Je suis en plein dedans, arrivé sur un projet à 2.4% de couverture de code par les tests, on arrive péniblement à 20% après 3semaines (tests unitaires et intégrations codés en // d'autres tâches) Et le bug = test , je l'applique également oui. En attendant, j'essaie d'expliquer que les tests permettent également de comprendre le métier, soulever les pb de specs, etc...mais quand on a la tête dans le guidon, on fait parfois les mauvais choix pour gagner du temps, gagner du temps sur les tests me semble une mauvaise idée.
  • Excellent article, qui parle d'un problème que j'essaye justement de mettre en évidence en ce moment chez mon client, et pour lequel je propose la même solution, avec ou sans Spring en plus. Petite remarque sur le code inclus dans l'article, certainement une coquille : la méthode "CreateCustomer" n'est plus "static" après le premier refactoring. @Waddle : un singleton peut très bien avoir un constructeur public. Et fournir un setter public dans un singleton static n'est pas non plus la meilleur approche. C'est un **Objet** qui est un singleton dans un contexte donné, pas une classe. On peux avoir 2 singletons, instances de la même classe, mais configurés chacun de façon différentes pour des besoins différents, et gérés par des Factories (de singleton) différentes. On ne renforcera en quelque sorte alors l'unicité du singleton que par convention, plutôt que par la protection de visibilité fournie par le langage. L'avantage étant de pouvoir maitriser le cycle de vie du singleton, pour augmenter la testabilité par exemple. En plus, on s'offre alors la possibilité de réutiliser du code et/ou de configurer des singletons de façon différentes pour les tester dans des configurations différentes. On peux voir ça dans Spring, qui par défaut est une factory de beans singletons, bien qu'on puisse avoir plusieurs instances de la factory. Un singleton est donc bien contextuel. Réduire le contexte à un champs statique, et mettre un constructeur en private, c'est s'empêcher de faire évoluer le code et de pouvoir le tester en toute liberté. Ceci dit, comme le dis Marc, il s'agit de faire un refactoring minimaliste et par étape, cette étape n'est peut être pas la cible, mais elle permet de faire évoluer le code dans le bon sens à moindre coût.
  • @Yann : extrait de la bible : "The Singleton Pattern is a Creational pattern. In programming, you may need to make sure that there is only one instance of a class running" Si tu as plusieurs instances d'une même classe qui sont uniques selon un context, il s'agit du pattern fly-weight. A quoi peut bien servir de rendre privé un constructeur vide sensé protéger de l'instanciation (pattern singleton) pour en offrir un autre qui prend un paramètre ? Les tests n'ont qu'à appeler le getInstance et utiliser un accesseur pour pousser les instances. Comme vous le dite, c'est précisément ce que l'on fait avec Spring en Java ou Unity en .NET. Vous dites qu'un setter public dans un singleton static (ah bon ? y a des singleton static et non-static ?) est une mauvaise pratique, pourriez-vous expliquer pourquoi ? Vous n'aimez pas l'encapsulation ? Pouvez-vous aussi expliquer pourquoi un constructeur privé empêche de tester ?
  • @Waddle : Merci pour la remarque Dans la description : « The Singleton Pattern is a Creational pattern. In programming, you may need to make sure that there is only one instance of a class running » avec laquelle je suis entièrement d'accord, mais que j'interprète de façon différente. ..., il est écrit "may need" et non pas "must need". De plus, l'objectif est bien de diminuer le code static dans le code existant souffrant de trop de code static. Je préfère préconiser que le code static ne doit être utilisé qu'en cas d'absolu nécessité, et l'absolu nécessité est en théorie extrêmement rare (bien qu'en pratique elle soit un peu moins rare, par exemple pour du refactoring par étape, comme celui de Marc) Généralement, un DAO n'as pas besoin d'être un singleton. L'implémentation d'un service business n'as pas non plus besoin d'être un singleton (la plupart du temps). Un EJB qui est backé par un service à lui même un cycle de vie (et un nombre d'instances) configurable par le serveur d'application. Si on souhaite faire un singleton, on configurera alors de préférence le serveur d'application. Si on utilise pas EJB, alors on utilisera une factory, qui fera la gestion du singleton, ou retournera une nouvelle instance à chaque fois. L'encapsulation de la création des objets, du cycle de vie, et du nombres d'instance, serait alors géré par la factory. Si on utilise un singleton dans un test, le singleton est alors instancié par la première méthode de test, et les méthodes de test suivantes réutilisent alors le même objet crée. Hors, dans un test unitaire, on doit pouvoir décider (si on en ressent le besoin, car c'est l'objet du test) de travailler avec une instance fraiche à chaque méthode de test, l'objet étant crée dans le setUp() Un test unitaire doit fonctionner de la même façon quelque soit l'ordre d'exécution des méthodes de test (sauf éventuellement TestSuite). Il y a quelques nuances entre singleton Statefull et Stateless. Bref, ce n'est pas tellement l'objet du billet. On va un peu au delà. Je pense qu'en sixième règle, on pourrait proposer : "recommander à partir de la troisième règles sur toutes les classes qui utilisent MyService.getInstance()" ce qui par refactoring successifs supprimerait le besoin de pas mal de singletons. Concernant les setters dans un singleton, ça casse l'encapsulation de toute façon. Et le constructeur par défaut aura probablement déjà instanciés ses dépendances dans le constructeur par défaut, alors que justement on ne veut pas les instancier, mais en injecter d'autres dans le test unitaire. J'espère avoir répondu à tes questions @Waddle (on peut se tutoyer?)
  • Effectivement on a un peu dépasser le sujet du billet. On peut se tutoyer :-) Pour le reste, je ne suis pas du tout d'accord avec ce que tu dis. A mon avis, tu mélanges beaucoup de choses : pattern, implémentation, cycle de vie, injection de dépendance, instanciation de dépendances. Le may need n'est pas interprétable dans le sens que tu penses. Ils n'ont pas dit : "The Singleton Pattern is a Creational pattern. In programming, when you need to make sure that there may be only one instance of a class running" Le Singleton n'a rien à voir avec du code static mais avec le nombre d'instance(s) d'une classe. C'est un peu comme dire que l'héritage est là pour réutiliser du code. Quand tu dis "une DAO n'a pas besoin d'être un singleton", tu te trompes et tu as raison à la fois. Rien n'a besoin d'être un Singleton, juste, c'en est ou ce n'en est pas. Et en l'occurrence, une DAO et les services en sont. Encore une fois, si ton implémentation de DAO peut exister en plusieurs versions selon une configuration, c'est un flyweight et non un singleton (encore que je vois mal comment cela pourrait arriver). Pour les tests, si tu as un Singleton, tu peux le modifier comme tu le souhaite ou le réinitialisé, je ne vois pas le problème. Chaque test est responsable de son initialisation, d'où le setUp. Aucun pb ici. Pour les setters, ils SONT l'encapsulation. Un constructeur vide n'instancie pas ses dépendances sinon cela implique un couplage fort entre ton singleton et ses dépendances.
  • Ouaip, tu as raison, je me trompe surement en disant qu'il peut y avoir plusieurs singletons, parce que effectivement la définition de singleton, c'est qu'il est unique. Cependant, dans le contexte du billet, Singleton est utilisé pour aider a faire un refactoring, pas pour renforcer le fait qu'il ne doit exister qu'une seul instance de la classe refactorée. Ainsi on pourra changera partout les appels de "MyService.doStuff(...)" par "MyService.getInstance().doStuff(...)" avec un FindAndReplace. mais rien n'empêche de faire "new MyService(myDao).doStuff()" Après, il existe effectivement des cas ou ça ne reviendra pas exactement au même (Singleton Statefull ou encapsulant des ressources "heavy weight") et là effectivement, il faudra protéger l'accès par un modifier private, .... ou alors ... simplement documenter que le code applicatif doit utiliser le singleton, et que seul les tests peuvent créer des nouvelles instances. Cependant, le singleton "tel qu'on l'entends couramment" (basé sur un field static protégé par la classe qui est instanciée par le singleton, le plus facile a écrire) est unique pour une VM / ClassLoader. En environnement distribué, ce singleton sera en plusieurs exemplaires. Ce n'est donc plus un singleton. Je suis convaincu qu'il existe plusieurs façons de gérer un Singleton. Tout est une histoire de contexte (ClassLoader, VM, Cluster, ...) et j'ajoute donc qu'une partie logique applicative peut désigner un objet "Singleton" car c'est un singleton pour cette partie logique applicative, mais pas un singleton pour le système (ClassLoader, VM, Cluster, ...) Le design pattern donne peut-être un exemple d'implémentation de Singleton, mais cela ne signifie pas que ce soit la seule façon possible. Je dis peut être une grosse bêtise. Si c'est le cas, alors ca veut dire que effectivement, je ne fais pas de Singleton au sens strict du terme. Ceci car je trouve le pattern Singleton au sens strict pas assez testable. Le pattern Fly Weigth avec un pool de taille 1 est effectivement proche de ce que je présente, mais il serait alors utilisé pour ne créer qu'une seule instance, ce qui reviendrait à un Singleton quand même. Abus de langage? Peut être. En d'autre terme, si un Objet unique globalement est géré par une autre classe (ou objet) que la classe instanciée par cet Objet, est ce un Singleton ou un Fly Weight? D'autres avis?
  • J'ai un avis souvent simpliste, désolé ;-) Un singleton doit être unique, d'où l'implémentation avec un champs static et la méthode getInstance(). J'irais donc plutôt dans le sens de Waddle tel que je l'ai compris. Mais j'irais plus loin : il me semble qu'un setter static public irait à l'encontre de ce principe de singleton car il faudrait un constructeur public et l'on ne pourrait donc plus assurer le "singleton". Du coup, si l'on reste sur cette théorie, ça n'aide pas vraiment pour le test en question. J'aurais d'ailleurs tendance à penser qu'un singleton ou une classe avec des méthodes static ne changent pas grand chose… Dans l'exemple présenté, on crée un constructeur avec un IMyDao qui ne sera a priori jamais utilisé, sauf dans les tests, tout en "violant" le principe du singleton. Je porte un véritable intérêt sur les tests *unitaires* et j'ai encore du mal à "simplifier" trop le code et aller à l'encontre du principe "un bon code ne peut pas être mal utilisé" juste pour faciliter les tests. On doit pouvoir faire mieux non ?
  • Merci à tous pour vos remarques. @Yann : Il s'agit effectivement d'une coquille. Après le premier refactoring la méthode CreateCustomer n'est plus statique. @Waddle et Yann : Passer de méthodes statiques à un singleton était le premier pas permettant d'avancer. Ce n'était pas une cible en soit. En démarrant un projet de zéro, je me serais probablement orienté vers un système permettant l'injection de dépendance comme Spring ou Unity car cet exemple implique un couplage fort avec les dépendances comme le précise Waddle. @Sylvain, Waddle et Yann : Effectivement je suis d'accord j'ai employé la notion singleton avec un léger abus de langage car je ne garantis pas une seule instance. Mais l'utilisation d'un singleton ici avait pour seul objectif de rester le plus proche possible d'une classe avec des méthodes statiques afin de minimiser les changements. @Waddle Concernant le choix d'un second constructeur par rapport à un setter pour pouvoir modifier le DAO, j'ai privilégié le constructeur en voulant éviter que ce setter soit mal interprété et mal utilisé sur l'instance de production (par exemple en settant une nouvelle instance de DAO mal configurée). L'avantage d'un constructeur est qu'il fige le comportement pour l'instance et ne peut par corrompre celle du singleton ou la remplacer dans le cas d'un setter statique public. Grâce à vos remarques, je me rends compte que le constructeur public avec la notion de singleton a posé un problème de compréhension. Je vais pense corriger mon exemple comme ceci : - marquer ce second constructeur internal (ce qui garantit l'unicité de l'instance pour tout client de cette API) - ajouter un commentaire explicite sur le second constructeur "Ce constructeur est uniquement dédié aux tests unitaires et n'est pas conçu pour être utilisé en temps normal" - placer l'attribut [assembly: InternalsVisibleTo("UnitTests")] sur mon assembly principale pour pouvoir accéder au constructeur dans les tests. L'autre solution serait de rendre le constructeur privé et d'utiliser systématiquement la classe Private Object pour y accéder dans les tests unitaires. Je trouver la visibilité internal suffisamment parlante pour éliminer l'ambiguïté à l'utilisation et inciter le développeur à lire plus attentivement le commentaire. Qu'en pensez-vous? @Waddle : Concernant la 6ème règle cela va un peu au delà du billet tel que je l'avais prévu mais c'est très intéressant. :-) Remplacer la propriété Instance permettrait de supprimer les singletons mais ça n'est pas selon moi un fin en soit. Je l'utiliserai plutôt pour masquer l'arrivée progressive d'un framework d'injection de dépendance, chaque méthode getInstance() pouvant déléguer au contexte. D'autres idées pour faire cohabiter (le refactoring ne se fait pas en un jour...) classes à méthodes statiques, singleton et contexte sans se brûler les doigts? @Sylvain : Je pense qu'il est possible de faire du "bon code qui ne puisse pas être mal utilisé" tout en le rendant testable. Est-ce que la correction vous semble aller dans le bon sens? Auriez-vous d'autres idées pour concilier les deux?
    1. Laisser un commentaire

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


      Ce formulaire est protégé par Google Recaptcha