InfOsaurus

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

Tag - Architecture

Fil des billets - Fil des commentaires

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 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