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.
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.
Au passage, certains auront reconnu la notion de tests de contrat définie par J. B. Rainsberger dans "Integration tests are a Scam".
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





