InfOsaurus

Aller au contenu | Aller au menu | Aller à la recherche

mardi 21 août 2012

TDD et clean Architecture, partie 3

Retour au format article pour cette 3ème et dernière partie sur Clean Architecture. Je vous recommande au passage un très bon billet qu'Uncle Bob vient de publier et qui résume un peu tout ce qu'il a produit sur cette approche jusqu'à présent.

Pour rappel, les 3 principales couches dans ce type d'architecture sont les entités, les interacteurs et les bornes. La dernière fois, nous avons implémenté les interacteurs (use cases) pour notre application FigurineShop en utilisant TDD. Cette fois-ci nous allons nous intéresser aux bornes et à ce qu'il y a derrière.

Clean Architecture

Si vous vous rappelez bien, dans l'épisode précédent nous avions des tests de collaboration pour nos interacteurs qui ressemblaient à ceci :

[Test]
public void InteracteurPeutRetournerListeFigurinesContenueDansEntrepot()
{
    var listeFigurines = new List<Figurine>
                             {
                                 new Figurine("Tintin"),
                                 new Figurine("Milou"),
                                 new Figurine("Capitaine Haddock")
                             };
    doublureEntrepot.Setup(d => d.ToutesFigurines()).Returns(listeFigurines);
 
    Assert.AreEqual(listeFigurines, interacteur.Catalogue());
}

On avait laissé les dépendances des interacteurs (entrepôts...) à l'état purement abstrait et on les avait mockées pour pouvoir tester uniquement la collaboration des interacteurs avec ces dépendances et pas ces dépendances elles-mêmes.
Jusqu'alors nous avions donc uniquement des interfaces pour ces dépendances, ce sont les bornes. Comment compléter le puzzle et implémenter les parties noires sur le schéma ci-dessus ? Et bien il suffit juste de prendre chacune de ces interfaces qui existaient pour l'instant seulement à l'état de mock et de créer son ou ses implémentations concrètes.


Mais attention, nous sommes en TDD, il faut donc commencer par un test ! Lequel ?

En l'occurrence, une chose devrait nous frapper. Nous avons précédemment écrit des tests de collaboration pour vérifier que les interacteurs communiquaient bien avec leurs dépendances en supposant que les dépendances se comportaient d'une certaine manière, et nous avons programmé nos doublures pour qu'elles agissent de cette manière. Mais nous n'avons pas encore écrit de test exigeant que les implémentations concrètes de ces dépendances se comportent réellement ainsi ! C'est ce que nous allons faire en écrivant le reflet, le symétrique des tests de collaborations mais dans l'autre sens. En d'autres termes : nous avons testé que la classe A appelle correctement une méthode de la borne abstraite B avec certains paramètres et sait gérer ses réponses. Maintenant, il faut tester que chaque implémentation de B

  • Est bien capable de répondre aux sollicitations de A avec ces paramètres,
  • Et qu'elle se comporte de telle sorte qu'elle renvoie bien ces fameuses réponses.

Tests de collaboration et de contrat

Au passage, certains auront reconnu la notion de tests de contrat définie par J. B. Rainsberger dans "Integration tests are a Scam".

Miroir (c) wreckedm 2006 http://www.sxc.hu/photo/668894

Cela parait complexe et théorique, mais en réalité c'est très facile. Pour l'instant la seule borne mockée dans les tests des interacteurs était IEntrepotFigurines, la source de données dans lequel l'interacteur allait piocher ses figurines. On avait 3 tests de collaboration :

  • Un qui testait si la méthode ToutesFigurines() de l'entrepôt était appellée par l'interacteur,
  • Un qui vérifiait que l'interacteur se comportait comme atendu quand ToutesFigurines() retournait une liste vide,
  • Et un autre qui vérifiait qu l'interacteur se comportait comme atendu quand ToutesFigurines() retournait une liste remplie.

Le premier test ne nécessite pas de "test symétrique" côté entrepôt : la méthode ToutesFigurines() n'a pas de paramètre donc on se doute que l'entrepôt va accepter les appels à cette méthode dès lors qu'il implémente le contrat (ça aurait nécessité plus de vérifications si la méthode avait eu des paramètres, et notamment des paramètres qu'on peut mettre à null). Il reste deux tests symétriques à créer :

  • Un qui teste si l'entrepôt est capable de retourner une liste vide dans certaines circonstances (symétrique du 2è test),
  • Un qui teste si l'entrepôt peut retourner une liste pleine dans d'autres circonstances (symétrique du 3è test).

Pour plus de facilité, on va ici utiliser un entrepôt qui conserve les figurines en mémoire. Dans la vraie vie, on utiliserait évidemment un entrepôt qui les conserve dans un stockage persistant (base de données)...

[TestFixture]
public class EntrepotFigurinesMémoireTest
{
    [Test]
    public void RetourneListeVideQuandPasDeFigurinesDansLEntrepot()
    {
        IEntrepotFigurines entrepotFigurines = new EntrepotMémoireFigurines();
 
        Assert.That(entrepotFigurines.ToutesFigurines(), Is.Empty);
    }
 
    [Test]
    public void RetourneListeDeFigurinesContenuesDansLEntrepot()
    {
        var tintin = new Figurine("Tintin");
        var milou = new Figurine("Milou");
        var haddock = new Figurine("Haddock");
        var listeFigurines = new List<Figurine> { tintin, milou, haddock };
        IEntrepotFigurines entrepotFigurines = new EntrepotMémoireFigurines(listeFigurines);
 
        Assert.That(entrepotFigurines.ToutesFigurines(), Is.EqualTo(listeFigurines));
    }
}

Par bonheur, ces deux tests de contrat avec l'extérieur suffisent à définir ce que doit faire l'entrepôt tout entier. L'implémentation suivante fait passer les tests :

public class EntrepotMémoireFigurines : IEntrepotFigurines
{
    protected IList<Figurine> figurines = new List<Figurine>();
 
    public EntrepotMémoireFigurines(IList<Figurine> figurines)
    {
        this.figurines = figurines;
    }
 
    public IList<Figurine> ToutesFigurines()
    {
        return figurines;
    }
}

L'autre partie des bornes se trouve côté mécanisme d'acheminement, c'est à dire interface utilisateur.
Nous allons créer un Présenteur par action utilisateur (consulter le catalogue, consulter le panier et ajouter une figurine au panier) qui va appeler l'interacteur approprié et retourner le résultat. Les présenteurs manipulent en temps normal des structures de données adaptées au mécanisme d'acheminement mais par souci de simplicité nous utiliserons directement les entités métier (ce qui fait que nos présenteurs peuvent apparaître comme de simple passe-plats, ce qu'ils sont).

Voici les tests des présenteurs :

[TestFixture]
public class PresenteurCatalogueTest
{
    [Test]
    public void PresenteurCommuniqueAvecInteracteurCatalogue()
    {
        var interacteur = new Mock<IInteracteurCatalogue>();
        var presenteur = new PresenteurCatalogue(interacteur.Object);
 
        presenteur.Catalogue();
 
        interacteur.Verify(i => i.Catalogue(), Times.Once());
    }
 
    [Test]
    public void PresenteurRetourneCatalogueQueLuiADonnéInteracteur()
    {
        var interacteur = new Mock<IInteracteurCatalogue>();
        var presenteur = new PresenteurCatalogue(interacteur.Object);
 
        var figurines = new List<Figurine> { new Figurine("Tintin"), new Figurine("Milou"), new Figurine("Haddock") };
        interacteur.Setup(i => i.Catalogue()).Returns(figurines);
 
        Assert.AreEqual(figurines, presenteur.Catalogue());
    }
}
 
[TestFixture]
public class PresenteurAjoutFigurineTest
{
    [Test]
    public void PresenteurAjoutFigurineAppelleInteracteur()
    {
        var interacteur = new Mock<IInteracteurAjoutFigurine>();
        var presenteur = new PresenteurAjoutFigurine(interacteur.Object);
 
        var figurine = new Figurine("Tintin");
        presenteur.AjouterFigurine(figurine);
 
        interacteur.Verify(i => i.AjouterFigurine(figurine), Times.Once());
    }
}
 
[TestFixture]
public class PresenteurPanierTest
{
    [Test]
    public void PresenteurAppelleInteracteurPanier()
    {
        var interacteur = new Mock<IInteracteurConsultationPanier>();
        var presenteur = new PresenteurPanier(interacteur.Object);
 
        presenteur.Update();
 
        interacteur.Verify(i => i.ConsulterPanier(), Times.Once());
    }
 
    [Test]
    public void PresenteurContientLesBonnesDonnées()
    {
        var interacteur = new Mock<IInteracteurConsultationPanier>();
        var lignesPanier = new Dictionary<Figurine, int>()
                               {
                                   {new Figurine("Tintin"), 1},
                                   {new Figurine("Milou"), 2}
                               };
        var total = 32f;
        var reponse = new ModeleReponsePanier(lignesPanier, total);
        interacteur.Setup(i => i.ConsulterPanier()).Returns(reponse);
        var presenteur = new PresenteurPanier(interacteur.Object);
 
        presenteur.Update();
 
        Assert.AreEqual(lignesPanier, presenteur.LignesPanier);
        Assert.AreEqual(total, presenteur.MontantPanier);
    }
}

Enfin, il faut une interface graphique pour afficher l'application. J'ai choisi une application console mais cela pourrait être du web ou du client lourd en réutilisant les mêmes présenteurs.

L'application console contient :

  • La méthode Main() qui sert de point de départ et de Composition Root pour assembler les différentes classes entre elles.
  • Des méthodes pour afficher le menu et les différents "écrans"

class Program
{
    static void Main(string[] args)
    {
        var panier = new PanierFigurines();
        var entrepotFigurines = new EntrepotMémoireFigurinesDémo();
        var presenteurCatalogue = new PresenteurCatalogue(new InteracteurCatalogueFigurines(entrepotFigurines));
        var presenteurAjoutFigurine = new PresenteurAjoutFigurine(new InteracteurAjoutFigurinePanier(panier));
        var presenteurPanier = new PresenteurPanier(new InteracteurConsultationPanier(panier));
 
        AfficheMenu();
        var input = string.Empty;
        while ((input = Console.ReadLine()) != "4")
        {
            switch (input)
            {
                case "1":
                    AfficheCatalogue(presenteurCatalogue);
                    break;
                case "2":
                    AffichePanier(presenteurPanier);
                    break;
                case "3":
                    SaisieFigurine(presenteurAjoutFigurine, presenteurCatalogue);
                    break;
                default:
                    Console.WriteLine("Mauvaise saisie");
                    break;
            }
            AfficheMenu();
        }
    }
 
    private static void SaisieFigurine(PresenteurAjoutFigurine presenteurAjoutFigurine, PresenteurCatalogue presenteurCatalogue)
    {
        Console.Write("Numéro de la figurine dans le catalogue ? :");
        var numero = int.Parse(Console.ReadLine());
        if (numero < 1 || numero > presenteurCatalogue.Catalogue().Count)
        {
            Console.WriteLine("Saisie invalide.");
            return;
        }
        var figurine = presenteurCatalogue.Catalogue()[numero - 1];
        presenteurAjoutFigurine.AjouterFigurine(figurine);
        Console.WriteLine("Figurine ajoutée.");
    }
 
    private static void AffichePanier(PresenteurPanier presenteurPanier)
    {
        Console.WriteLine();
        presenteurPanier.Update();
        if (presenteurPanier.LignesPanier != null)
            foreach (var lignePanier in presenteurPanier.LignesPanier)
                Console.WriteLine(lignePanier.Key.Nom + " .................... " + lignePanier.Value);
        Console.WriteLine("---------------------------------");
        Console.WriteLine("Total : " + presenteurPanier.MontantPanier + " Euros");
        Console.WriteLine();
    }
 
    private static void AfficheCatalogue(PresenteurCatalogue presenteurCatalogue)
    {
        Console.WriteLine();
        foreach (var figurine in presenteurCatalogue.Catalogue())
            Console.WriteLine(presenteurCatalogue.Catalogue().IndexOf(figurine) + 1 + "." + figurine.Nom);
        Console.WriteLine();
    }
 
    private static void AfficheMenu()
    {
        Console.WriteLine("=================================");
        Console.WriteLine("1. Consulter le catalogue");
        Console.WriteLine("2. Consulter le panier");
        Console.WriteLine("3. Ajouter une figurine au panier");
        Console.WriteLine("4. Sortir");
        Console.WriteLine("=================================");
        Console.WriteLine();
        Console.Write("Choix ? : ");
    }
}

Comme d'habitude, vous pouvez retrouver tout le code de l'application et les tests sur Github : http://github.com/infosaurus/FigurineShop

dimanche 1 avril 2012

Code Retreat Montpellier

Le 24 Mars dernier se déroulait le premier Code Retreat à Montpellier, animé par Antoine Vernois et organisé par Julien Lafont, Vivian Pennel et Sylvain Fraïssé. L'événement avait lieu dans les locaux de l'école Epitech. Je ne vais pas rappeler les règles d'un code retreat en détail, mais il s'agit d'une rencontre où on développe en pair programming sur un problème donné - en général le Conway's Game of Life, sous forme d'itérations de 45 minutes en changeant de binôme à chaque fois. Voici mon petit retour (tardif) sur la journée.

Ce que j'ai aimé :

  • La bonne humeur générale propice aux échanges, avec des développeurs venus d'un peu partout (Montpellier mais aussi Marseille, Toulouse, etc).
  • La variété des langages présents, de Java à Python en passant par Scala, Ruby et plein d'autres.
  • Le cadre d'Epitech sympa, dans une bâtisse ancienne montpelliéraine rénovée.
  • L'efficacité d'Antoine qui avait visiblement reçu 5/5 l'unique conseil avisé de Corey Haines :)

Ce que j'ai appris :

  • Le pouvoir des contraintes. Se mettre des restrictions très pénibles (pas de if, de boucles, pas de getters/setters...) force en fait à réfléchir autrement et paradoxalement on en tire plein d'enseignements à mettre en application plus tard.
  • Un Code Retreat, ça passe très vite, aussi bien la journée que les itérations elles-mêmes. Au bout des 45 minutes, on est systématiquement frustré d'être si loin de voir notre monde en vie, surtout quand de précieuses minutes ont été passées en contingences matérielles annexes. Mais c'est le jeu, ma pauvre Lucette :)
  • Ruby, ça a l'air de roxxer. Il faut que je m'y penche de plus près.
  • Décidément, j'ai du mal à convaincre mes petits camarades que C#, c'est bien. Pourtant, C#, c'est bien ;)

Si c'était à refaire :

  • Eviter de se pointer avec un langage ou un environnement qu'on maîtrise mal. Je pense qu'il est crucial qu'un des deux membres du binôme connaisse très bien la techno utilisée pour pouvoir se concentrer uniquement sur les tests et l'émergence du design. J'ai le sentiment d'avoir passé trop de temps à cerner des détails de langages, c'est intéressant mais pas forcément dans les clous de la journée.
  • Peut-être expliquer un peu mieux au début de la journée ce qu'est TDD, ou demander aux gens de se renseigner dessus avant. J'ai l'impression qu'il y avait un taux assez haut de "totale découverte TDD" dans la salle ce qui est bien sûr enrichissant au niveau du partage, mais a peut-être généré une certaine perplexité.

Dans la catégorie "j'ai testé pour vous" :

  • Casser la cafetière du code retreat dès le matin (encore désolé Julien)... j'ai un peu fait mon boulet sur ce coup-là.
  • Le Conway's Game of Life en Python avec un binôme qui n'avait fait que du C et pas familier du développement objet. Tout ça sans if et sans boucle sinon c'est pas drôle... Ou comment expliquer le polymorphisme à quelqu'un en 5 minutes :)
  • Le ping-pong programming à 3 sans avoir le droit de se parler et avec un test runner JUnit qui crashe de temps en temps. Un grand moment... d'incompréhension, mais très instructif sur notre dépendance à la communication !


Debriefing entre deux sessions

En tout cas, très bonne expérience que ce premier code retreat, encore merci aux organisateurs et aux participants ! A quand la prochaine ? ;)

dimanche 18 mars 2012

TDD et Clean Architecture, partie 1

Après le kata, 2è vidéo sur TDD avec une petite appli d'exemple et un focus sur un type d'architecture particulier :





  • Vous pouvez retrouver la présentation d'Uncle Bob ici

mardi 29 novembre 2011

Tintin, course de haies et TDD

Aujourd'hui je me lance dans un petit kata en vidéo :



N'hésitez pas à me faire part de vos commentaires... Vous pouvez aussi retrouver le code sur GitHub : http://github.com/infosaurus/KataTintin

lundi 17 mai 2010

Bientôt l'été ! Quelques recettes pour faire mincir... ses TU

Régime

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      EntrepotLivres entrepotLivres = new EntrepotLivres();
  5:      Auteur evans = new Auteur("Evans", "Eric");
  6:      CategorieLivre categorie = new CategorieLivre("Développement");
  7:      Adresse adresse = new Adresse("55 rue des Pommiers");
  8:      Librairie librairie = FabriqueLibrairie.Creer("Librairie la Pomme d'Or", adresse);
  9:      IList<Librairie> librairies = new List<Librairie>() { librairie };
 10:      Livre domainDrivenDesign = FabriqueLivre.Creer("Domain Driven Design", evans,
 11:          categorie, librairies);
 12:      Livre autreLivre = FabriqueLivre.Creer("autre livre", evans, categorie, librairies);
 13:      entrepotLivres.Ajouter(domainDrivenDesign);
 14:      entrepotLivres.Ajouter(autreLivre);
 15:   
 16:      IList<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 17:   
 18:      Assert.AreEqual(2, livresRetournes.Count);
 19:      Assert.Contains(domainDrivenDesign, livresRetournes);
 20:      Assert.Contains(autreLivre, livresRetournes);
 21:  }

 

Mais que fait donc ce test unitaire ?

C'est la question que je me suis posée récemment en relisant un test du même genre.

Le titre peut nous donner une indication, mais le corps de ce test est lui-même peu lisible. Et même sans rentrer dans le détail du code, il y a fort à parier que la maintenabilité et la performance ne seront pas au rendez-vous avec ce gros bloc d'initialisation (le Arrange de Arrange Act Assert) qui plombe le test.

Le problème derrière tout ça, c'est que pour tester un comportement basique d'un objet (ici un Repository de livres), on est obligé de le remplir en initialisant toute une grappe d'autres objets (des livres, des auteurs, catégories...) assez complexes à construire. DDD ne nous facilite pas vraiment la tâche puisque si l'on veut assurer les invariants et garantir l'état valide du domaine, le seul point d'accès pour créer un objet complexe est normalement la Fabrique. Celle-ci va initialiser l'objet dans un état valide et demander beaucoup de paramètres, dont potentiellement d'autres objets qui doivent être construits eux aussi et... vous l'avez compris, le test devient rapidement surchargé.

 

Respectons l'esprit de TDD

 

Dans son livre XUnit Test Patterns et sur le site xunitpatterns.com, Gerard Meszaros met un nom sur ce genre de code smell : Obscure Test. Il propose une approche de l'écriture de tests fidèle aux principes de TDD pour éviter cet écueil :

« Ecrire les tests d'une manière "outside-in" peut nous éviter de produire des test obscurs qu'il faudrait ensuite refactorer. Dans cette approche, on commence par tracer les contours d'un Test à 4 Phases en utilisant des appels à des Méthodes Utilitaires de Tests non existantes.
Une fois qu'on est satisfait des tests, on peut commencer à écrire les méthodes utilitaires dont on a besoin pour les exécuter. En écrivant les tests d'abord, on obtient une meilleure compréhension de ce que les méthodes utilitaires doivent nous apporter pour rendre l'écriture des tests aussi simple que possible. »

En d'autres termes, on va créer le test le plus simple et le plus lisible possible dès le départ en s'aidant de méthodes utilitaires très expressives. Adieu la grappe d'objets, sa complexité sera cachée dans nos méthodes :

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      Livre domainDrivenDesign = CreerLivre("Domain Driven Design");
  5:      Livre autreLivre = CreerLivre("Autre livre");
  6:      EntrepotLivres entrepotLivres = CreerEntrepotAvecDeuxLivres(domainDrivenDesign,
  7:          autreLivre);
  8:   
  9:      List<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 10:   
 11:      Assert.AreEqual(2, livresRetournes.Count);
 12:      Assert.Contains(domainDrivenDesign, livresRetournes);
 13:      Assert.Contains(autreLivre, livresRetournes);
 14:  }

 

Et voici l'implémentation des méthodes utilitaires :

 

   1:  protected Livre CreerLivre(string titre)
   2:  {
   3:      Auteur auteur= new Auteur("Nom", "Prénom");
   4:      CategorieLivre categorie = new CategorieLivre("Une catégorie");
   5:      Adresse adresse = new Adresse("Une adresse");
   6:      Librairie librairie = FabriqueLibrairie.Creer("Une librairie", adresse);
   7:      IList<Librairie> librairies = new List<Librairie>() { librairie };
   8:      Livre livre = FabriqueLivre.Creer(titre, auteur, categorie, librairies);
   9:      return livre;
  10:  }
  11:   
  12:  protected EntrepotLivres CreerEntrepotAvecDeuxLivres(Livre livre1, Livre livre2)
  13:  {
  14:      EntrepotLivres entrepot = new EntrepotLivres();
  15:      entrepot.Ajouter(livre1);
  16:      entrepot.Ajouter(livre2);
  17:      return entrepot;
  18:  }

 

Coupons les cordons

 

Notre test est déjà beaucoup plus lisible, mais une autre question se profile à l'horizon. Est-il vraiment unitaire ? Peut-on dire qu'il est atomique alors qu'il s'appuie intensivement sur le bon fonctionnement d'objets externes, à savoir des Fabriques ?

Meszaros préconise de s'affranchir des dépendances aux objets annexes en recourant à des valeurs par défaut, ou en utilisant des Dummy Objects, des objets creux qui ne contiennent rien ou juste l'information nécessaire au test. Concrètement, on extrait une interface du vrai objet en question pour pouvoir créer des dummies sans contrainte de remplissage. Dans notre exemple c'est le Livre qui sera "dummifié" puisque l'Entrepot à tester contient directement des Livres :

 

   1:  public interface ILivre
   2:  {
   3:      ...
   4:  }
   5:   
   6:  public class DummyLivre : ILivre
   7:  {
   8:      //On construit un objet minimal
   9:      public DummyLivre(string titre)
  10:      {
  11:          this.titre = titre;
  12:      }
  13:  }
  14:   
  15:  //Nouvelle méthode du helper
  16:  protected DummyLivre CreerLivre(string titre)
  17:  {
  18:      return new DummyLivre(titre);
  19:  }

 

Cette solution ne me satisfait pas pleinement parce qu'il faut créer un interface ILivre et la faire implémenter à Livre, ce qui n'est pas forcément élégant au regard de la nécessité d'expressivité des classes du domaine dans DDD. A la place, on peut recourir à un framework d'isolation qui va instancier un proxy de notre Livre pour l'utiliser dans les tests sans besoin de recourir à une interface. Un exemple avec Moq :

 

   1:  protected Livre CreerLivre(string titre)
   2:  {
   3:      var mockLivre = new Mock<Livre>();
   4:      mockLivre.Setup(livre => livre.Titre).Returns(titre);
   5:      return mockLivre.Object;
   6:  }

 

NB : cela nécessite une petite modification de la classe Livre. Il faut rendre la property Titre virtual afin que Moq puisse la surcharger.

 

Stratégies d'extraction

 

Tout cela est très bien, mais nous avons toujours des méthodes utilitaires à l'intérieur de notre classe de test, ce qui n'est pas l'idéal pour la lisibilité et la réutilisabilité. Une solution pourrait être de rendre ces méthodes statiques et de les externaliser dans un Helper à part (on parle aussi de pattern ObjectMother) :

 

  1:  public static class LivreTestHelper
  2:  {
  3:      public static Livre CreerLivre(string titre) { ... }
  4:      public static EntrepotLivres CreerEntrepotAvecDeuxLivres(Livre livre1,
  5:          Livre livre2) { ... }
  6:  }

 

Voici maintenant à quoi ressemble le test :

 

  1:  [Test]
  2:  public void GetTousLivres_Retourne_Tous_Livres()
  3:  {
  4:      Livre domainDrivenDesign = LivreTestHelper.CreerLivre("Domain Driven Design");
  5:      Livre autreLivre = LivreTestHelper.CreerLivre("Autre livre");
  6:      EntrepotLivres entrepotLivres = LivreTestHelper.CreerEntrepotAvecDeuxLivres(
  7:          domainDrivenDesign, autreLivre);
  8:   
  9:      IList<Livre> livresRetournes = entrepotLivres.GetTousLivres();
 10:   
 11:      Assert.AreEqual(2, livresRetournes.Count);
 12:      Assert.Contains(domainDrivenDesign, livresRetournes);
 13:      Assert.Contains(autreLivre, livresRetournes);
 14:  }

 

Imaginons maintenant qu'on ait d'autres tests à écrire sur l'EntrepotLivres. Chacun va avoir besoin de son contexte d'initialisation particulier (livres avec une certaine catégorie, un certain auteur, un nombre de livres fixé...) Chaque test va ajouter ses méthodes au TestHelper et il y a fort à parier qu'on va vite se retrouver avec un helper obèse et beaucoup de duplication de code, chaque test ayant besoin d'un environnement légèrement différent des autres mais avec des choses communes.

C'est là que nous pouvons appeler le pattern Test Data Builder à la rescousse. Il va nous permettre de rendre modulaire la construction de notre contexte de test, sans trop nuire à la lisibilité.

Voici à quoi pourrait ressembler un test avec des DataBuilder :

 

   1:  [Test]
   2:  public void GetLivresParNomAuteur_Retourne_Bons_Livres()
   3:  {
   4:      Livre domainDrivenDesign = new LivreBuilder()
   5:          .AvecTitre("Domain Driven Design")
   6:          .AvecAuteur("Eric",  "Evans")
   7:          .Creer();
   8:   
   9:      Livre autreLivre = new LivreBuilder()
  10:          .AvecTitre("Autre titre")
  11:          .AvecAuteur("Autre", "Auteur")
  12:          .Creer();
  13:   
  14:      EntrepotLivres entrepotLivres = new EntrepotLivresBuilder()
  15:          .AvecLivre(domainDrivenDesign)
  16:          .AvecLivre(autreLivre)
  17:          .Creer();
  18:   
  19:      List<Livre> livresRetournes = entrepotLivres.GetLivresParNomAuteur("Evans");
  20:   
  21:      Assert.AreEqual(1, livresRetournes.Count);
  22:      Assert.Contains(domainDrivenDesign, livresRetournes);
  23:      Assert.IsFalse(livresRetournes.Contains(autreLivre));
  24:  }

 

Je ne sais pas ce que vous en pensez, mais je trouve ça plutôt agréable à l'oeil. Sous le capot, cela donne ça :

 

   1:  public class LivreBuilder
   2:  {
   3:      Mock<Livre> mockLivre = new Mock<Livre>();
   4:   
   5:     public LivreBuilder AvecTitre(string titre)
   6:      {
   7:          mockLivre.Setup(livre => livre.Titre).Returns(titre);
   8:          return this;
   9:      }
  10:   
  11:      public LivreBuilder AvecAuteur(string prenomAuteur, string nomAuteur)
  12:      {
  13:          var mockAuteur = new Mock<Auteur>();
  14:          mockAuteur.Setup(au => au.Prenom).Returns(prenomAuteur);
  15:          mockAuteur.Setup(au => au.Nom).Returns(nomAuteur);
  16:          mockLivre.Setup(livre => livre.Auteur).Returns(mockAuteur.Object);
  17:          return this;
  18:      }
  19:   
  20:      public Livre Creer()
  21:      {
  22:          return mockLivre.Object;
  23:      }
  24:  }
  25:   
  26:  public class EntrepotLivresBuilder
  27:  {
  28:      EntrepotLivres entrepotLivres = new EntrepotLivres();
  29:   
  30:      public EntrepotLivresBuilder AvecLivre(Livre livre)
  31:      {
  32:          entrepotLivres.Ajouter(livre);
  33:          return this;
  34:      }
  35:   
  36:      public EntrepotLivres Creer()
  37:      {
  38:          return entrepotLivres;
  39:      }
  40:  }

 

Un mot sur SetUp

 

On pourrait aussi être tenté d'instancier une partie du contexte des tests dans la méthode de SetUp de la classe de test (attribut [SetUp] avec NUnit).

Ce n'est pas forcément l'idéal en termes de lisibilité, et en tout cas on ne devrait construire dans cette méthode que les données qui sont le plus petit dénominateur commun entre tous les tests. En général, je préfère réserver le SetUp (et le TearDown) pour des opérations de plomberie générale des tests qui ne touchent pas au données, comme le démarrage et le rollback d'une transaction.

Dans ce sens, je suis assez d'accord avec Roy Osherove, auteur de The Art Of Unit Testing, qui écrit (p. 191) :

« Ma préférence personnelle est que chaque test crée ses propres mocks et stubs en appelant des méthodes de helpers dans le test lui-même, de façon à ce que le lecteur du test sache exactement ce qui se passe, sans avoir besoin de sauter du test au setup pour comprendre le tableau d'ensemble. »

Recette

Une alternative originale

 

Le Danois Mark Seemann propose une approche légèrement différente, qui fait partie d'un cortège de design patterns et conventions qu'il a appelé ZeroFriction TDD. Bien que je ne sois pas totalement persuadé par tous les patterns présentés (certains sont très orientés programmation défensive), un d'entre eux mérite qu'on s'y attarde : Fixture Object.

Seemann part du constat qu'en utilisant des helpers de tests statiques, on a forcément un contexte de test stateless, ce qui nous prive de certaines commodités. L'idée est de constituer un objet Fixture contenant l'état actuel des données de test et aussi la référence au système à tester (SUT). Ainsi le corps du test garde sa lisibilité et sa maintenabilité :

 

   1:  [Test]
   2:  public void GetTousLivres_Retourne_Tous_Livres()
   3:  {
   4:      EntrepotLivresFixture fixture = new EntrepotLivresFixture();
   5:   
   6:      fixture.AjouterPlusieursLivres();
   7:   
   8:      // System Under Test
   9:      EntrepotLivres sut = fixture.Sut;
  10:      List<Livre> resultats = sut.GetTousLivres();
  11:   
  12:      Assert.AreEqual(fixture.Livres, resultats);
  13:  }

 

On remarque que deux conventions de Zero Friction TDD censées reposer l'oeil et le cerveau du lecteur du test sont utilisées : Naming SUT Test Variables, qui préconise de nommer toujours de la même manière la variable de l'objet testé, et Naming Direct Output Variables, qui conseille la même chose pour le résultat qui va être asserté.

Voici l'implémentation du Fixture Object :

 

   1:  internal class EntrepotLivresFixture
   2:  {
   3:      public EntrepotLivresFixture()
   4:      {
   5:          Sut = new EntrepotLivres();
   6:          this.Livres = new List<Livre>();
   7:      }
   8:   
   9:      internal EntrepotLivres Sut { get; private set; }
  10:      internal IList<Livre> Livres { get; private set; }
  11:      internal int Plusieurs { get { return 3; } }
  12:      internal Livre PremierLivre { get { return Livres[0]; } }
  13:      internal Livre DeuxiemeLivre { get { return Livres[1]; } }
  14:   
  15:      internal void AjouterPlusieursLivres()
  16:      {
  17:          for (int i = 0; i < Plusieurs; i++)
  18:          {
  19:              Livre livre = new Mock<Livre>().Object;
  20:              Livres.Add(livre);
  21:              Sut.Ajouter(livre);
  22:          }
  23:      }
  24:  }

 

 

Voilà, ceci n'est qu'un échantillon des différentes stratégies de factorisation de tests imaginables. Il y a probablement des milliers de solutions possibles.

Et vous, quelle est votre régime minceur préféré pour vos tests ?