InfOsaurus

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

dimanche 30 septembre 2012

Visual Studio 2012

La version finale de Visual Studio 2012 étant sortie très récemment, j'ai pu le prendre en main et le tester pendant quelques heures. Je vous propose une petite visite guidée et commentée de cette nouvelle mouture de l'IDE.


Look & feel

Quand le sage lui montre la lune, le fou regarde les majuscules


C'est probablement le sujet qui a fait le plus jaser à la sortie de la RC de Visual Studio 2012 il y a quelques mois. Beaucoup se sont d'abord indignés contre les menus tout en lettres capitales dans la barre du haut. Franchement, je trouve la controverse un peu surfaite - les menus sont un tout petit détail de l'IDE, personnellement je vais assez peu dedans et le passage en majuscules ne m'a pas du tout gêné une fois la surprise initiale passée.

Penchons-nous plutôt sur la charte graphique et l'aspect général de cette nouvelle version.

Je trouve la page de démarrage simple, claire et lisible. C'est grosso modo la même que VS 2010 avec des informations mieux organisées et plus de matière sur les nouveautés de l'IDE. L'onglet Vidéos contient notamment des tutoriels instructifs sur ces dernières.


Page de démarrage

Dans tout l'IDE, Microsoft a fait le choix d'une interface graphique de type "ligne claire" à fort contraste avec très peu de couleurs et des zones carrées à la Metro Style. Les bords et les espaces non remplis, autrefois de couleur foncée, sont maintenant sensiblement de la même couleur que toutes les autres zones (éditeur de texte, explorateurs...) c'est à dire blancs ou gris très clair dans la charte par défaut. J'avoue que j'ai du mal avec ce style très lumineux (high key dirait-on en photographie) qui m'éblouit un peu. Les petits icônes, généralement noirs ou très peu colorés, sont aussi souvent peu reconnaissables et mal choisis.


Thème clair

J'ai donc essayé le thème sombre également proposé de base et... c'est un peu l'excès inverse, je suis vite revenu à l'autre.


Thèeme sombre

Du côté des performances, j'ai constaté une amélioration nette par rapport à VS 2010, que ça soit au lancement ou en termes de réactivité globale de l'IDE. Je n'ai pas non plus fait l'expérience de crashes intempestifs comme avec son prédécesseur à sa sortie. Tout paraît plus fluide - grâce à l'interface allégée ? Cela reste néanmoins à vérifier dans un contexte d'utilisation intensive, avec de nombreux fichiers ouverts, des designers UI, des tests unitaires et des recherches qui tournent, etc.

Au final, en bon adepte du dépouillement, je salue l'effort des concepteurs de VS 2012 pour simplifier l'habillage de l'outil et se concentrer sur l'essentiel ("La perfection, ce n'est pas quand il n'y a plus rien à ajouter mais plus rien à enlever", disait quelqu'un). Il va juste falloir trouver un thème plus confortable pour les yeux sensibles.


Navigation et éditeur de texte

Le retour de la vengeance des pin's


Autant le dire tout de suite, comparé à VS 2010, les innovations ne sont pas énormes en matière de navigation et d'édition du code dans ce cru 2012. On y trouve :

  • Une fonction "preview" très pratique. Il suffit de simple-cliquer sur un fichier dans l'explorateur de solutions pour afficher celui-ci comme si on l'avait ouvert. Simplement, son affichage est temporaire, un fichier en preview chasse l'autre. L'onglet du fichier preview s'affiche à droite, pour le distinguer des fichiers déjà ouverts. C'est une excellente fonction que je me suis retrouvé à utiliser énormément.


Fonction Preview

  • L'épinglage de fichiers, plus anecdotique. Les fichiers épinglés restent de façon permanente à gauche de la rangée d'onglets et ne sont jamais masqués même lorsqu'on a énormément de fichiers ouverts en même temps.


Pin's !

  • Un icône "Tout réduire" est maintenant présent par défaut dans l'explorateur de solution pour replier complètement l'arbre de la solution. Ce n'est pas trop tôt, cette fonctionnalité quasi-obligatoire à mon avis devant auparavant être ajoutée via un add-in ou une macro.


  • Surlignage de la ligne actuelle : elle peut paraître mineure, mais cette fonction apporte un confort non négligeable en nous indiquant instantanément où le code va s'insérer si on continue à taper au clavier. En effet, lorsqu'on part se balader de part et d'autre dans le fichier on perd souvent ce repère et le curseur n'est pas toujours clairement distinguable. Dommage que la ligne surlignée soit assez peu visible en thème clair, et plutôt trop visible en thème foncé.


Il y a aussi la fameuse zone de Lancement rapide qui se présente comme un champ de recherche "environnemental" pour retrouver un menu, une action, un fichier récemment ouvert...


Lancement rapide

L'idée est bonne sur le papier mais 95% de mes actions dans l'IDE étant des tâches familières déclenchées par des raccourcis clavier, j'en ai eu très peu l'utilité. Pour chercher dans des fichiers, je préfère utiliser l'excellent Ctrl-virgule apparu dans VS 2010. L'avenir nous dira si cela change.

En revanche, les manques de fonctions de productivité déjà signalés dans la version 2010 (mais pourtant présents depuis longtemps dans des outils tiers comme Resharper) sont toujours là :

- Toujours pas de "add reference and use"
- Toujours pas de fermeture automatique d'accolades
- Pas de suggestion de noms de variable
- Pas de goto base/inheritor depuis l'éditeur de texte
- etc.

Cette absence d'évolution est particulièrement criante dans le domaine du refactoring où il faut vraiment chercher pour trouver une nouveauté, à part peut-être une entrée de menu pour chercher de la duplication de code sur le code actuellement sélectionné (baptisé de manière amusante rechercher les clones en français).
Pendant ce temps, JetBrains nous ajoute des refactos à la pelle, dont le fameux Extract Class -Resharper reste à des années-lumières de ce que l'environnement de Microsoft sait faire par défaut.


Tests unitaires

Un petit pas pour le Shim, un grand pas pour l'humanité


Selon l'aveu même de représentants de Microsoft, dans un exercice d'auto-critique assez rare pour être souligné, l'équipe de Visual Studio s'est rendu compte que l'environnement de tests de 2010 était orienté testeurs et très peu pratique pour les développeurs.
En effet, le même type de projet Visual Studio servait pour les tests qualité et les tests unitaires des développeurs. Le test runner de VS 2010 était tout sauf confortable car lent, avec une interface verbeuse et peu visuelle obligeant à raisonner selon une notion de "liste de tests" peu familière. Tout cela a été corrigé dans VS 2012 où on a maintenant :

Un test runner digne de ce nom.

Il s'inspire bien sûr fortement des runners déjà existants, que ça soit celui de TestDriven.net, celui de Resharper ou d'autres. On y retrouve l'habituelle barre rouge ou verte ainsi que des pastilles des mêmes couleurs pour chaque test. A noter qu'on peut enfin écrire de nouveau tests et relancer le runner directement, il les prendra en compte sans besoin de passer par une fastidieuse opération d'ajout de tests à la liste. VS 2012 va même plus loin avec quelques fonctionnalités supplémentaires à saluer :

  • Une option permettant d'exécuter automatiquement les tests après chaque build. On est "presque" dans le continuous testing.
  • Dans ce mode, les tests les plus susceptibles d'échouer (ceux qui étaient déjà rouges, etc.) sont réexécutés en premier et les tests qui étaient verts ne sont pas réexécutés si d'autres échouent. On est "presque" dans l'intelligent test running à la JUnit Max.
  • On peut analyser la couverture de code d'un ou plusieurs tests.
  • Des raccourcis clavier sont présents de base pour exécuter un ou tous les tests (d'ailleurs on ne peut pas lancer des tests en faisant un clic droit depuis l'explorateur de solutions, il faut forcément utiliser un raccourci ou le test runner, radical mais étrange pour du Visual Studio...)


Test Runner

Plusieurs types de projet de test au lieu d'un seul.

Les tests d'intégration "pour les testeurs" ont été séparés des tests unitaires automatisés "pour les développeurs". Le Projet de test unitaire est à privilégier lorsqu'on veut faire du TU avec MSTest. Mais la bonne nouvelle, c'est qu'il n'y a même pas besoin d'utiliser ce type de projet pour tirer parti du test runner de Visual Studio, une bonne vieille bibliothèque de classes suffit pour peu qu'on ajoute les références adéquates.

Une plateforme unifiée pour tous les frameworks de test :

la VS Unit Test Platform. Elle fournit une API pour que des adaptateurs pour les différents frameworks de test puissent être développés (ceux de XUnit et NUnit sont déjà disponibles). Cela veut dire que virtuellement n'importe quel framework de test peut maintenant fonctionner avec le test runner de base de Visual Studio, alors qu'auparavant on devait installer soit des runners spécifiques aux frameworks, soit des runners génériques tiers (Gallio, Resharper...)


Un autre gros morceau de l'environnement de tests de VS 2012 est le framework Microsoft Fakes. Cet ambitieux composant inspiré du travail de MS Research sur Pex & Moles n'est ni plus ni moins qu'un framework d'isolation intégré à Visual Studio. Il permet de générer deux types de doublures de test : les Stubs et les Shims. Les premiers sont l'équivalent des Mocks des frameworks déjà existants et peuvent seulement remplacer des interfaces ou des classes abstraites, et les seconds sont plus lents mais peuvent mimer des classes concrètes (MS Fakes marche ici sur les plates-bandes d'outils payants comme TypeMock Isolator). Les Stubs sont recommandés pour mocker des classes de son propre projet alors que les Shims sont utiles comme doublures de classes d'assemblies tiers. On peut ainsi modifier des classes comme DateTime pour simuler une date courante différente, etc.
Au passage, on peut se demander où les concepteurs du framework sont allés chercher le terme Shim ("cale"), signe que le champ lexical de la doublure de test commence à sérieusement s'épuiser. Ceux qui avaient déjà du mal avec les fakes, stubs, mocks, dummies, etc., et on peut les comprendre, seront probablement désormais totalement perdus avec les nouvelles dénominations à la Microsoft....


Techniquement, la mise en oeuvre de ces doublures est assez inédite bien qu'un peu contraignante. Pour pouvoir utiliser MS Fakes, il faut sélectionner une référence à un assembly dans le projet de test et "Ajouter un assembly Fakes" :


Ajout d'assembly Fake

Une nouvelle DLL va alors se rajouter à nos références ; à l'intérieur, tous les Stubs et Shims possibles pour les types de l'assembly de base auront été générés et sont prêts à être utilisés dans nos tests. Malgré le surcoût initial d'ajouter l'assembly fake, cette approche est intéressante puisqu'il y a des chances que ces classes déjà présentes à la compilation soient plus performantes que des proxies générés au runtime, comme dans les frameworks d'isolation actuels.
Le setup des fakes se fait avec une syntaxe classique utilisant des lambdas. Pas de dépaysement de ce côté-là, en revanche un gros point noir : le framework ne semble pas proposer les méthodes d'assertion avancées de ses concurrents -vérifications sur le nombre d'appels d'une méthode d'un Stub, vérification simple des paramètres passés à une méthode, syntaxe fluent, etc.


Utilisation d'un Stub


Application Lifecycle Management

Du pain et de l'Agile


Je n'ai pas pu tester la partie ALM de Visual Studio 2012 par manque de serveur Team Foundation, mais le remaniement a l'air assez considérable et Scrum semble plus que jamais la méthodologie par défaut promue par Microsoft. On sent que l'équipe TFS a mis le paquet pour que Visual Studio, dans ses versions supérieures, devienne un centre complet de gestion du cycle de vie d'un projet.

Parmi les nouveautés que j'ai pu voir,

  • Une nouvelle vue Backlog plus claire (et pas juste un résultat de query TFS)
  • Un Task Board électronique présent par défaut, avec drag & drop et autres joyeusetés.
  • Un écran de planification de sprint
  • Un écran "My Work" dans Team Explorer avec les tâches de l'utilisateur
  • Un cadre pour les revues de code
  • Un système de gestion de feedback utilisateur


Task Board

En parallèle de cela, on peut noter que Microsoft a fait des efforts pour rendre ses outils plus compatibles avec la philosophie agile -notamment après les mini-vagues d'indignation qui ont eu lieu lors de la sortie des premières version beta de VS 2012. On a toujours la possibilité d'assigner les tâches à chaque développeur (façon Waterfall/micro-management) dès le sprint planning, mais ce n'est plus obligatoire ni recommandé (dans l'exemple donné dans la documentation TFS, l'équipe ne le fait pas). De même, j'ai l'impression que la référence au diagramme de Gantt qui apparaissait dans l'écran de planification de sprint a été enlevée.


Conclusion

Ce soleil qui ne voulait pas devenir une Etoile noire


Il y aurait bien d'autres choses à dire sur Visual Studio 2012, qui est un univers à lui tout seul. Comme d'habitude, Microsoft court après son écosystème - une foule de fournisseurs d'add-in tiers innovants - dont il "officialise" dans VS 2012 quelques fonctionnalités, tout en prenant bien garde à le faire assez lentement pour ne jamais rattraper tout son retard sur ces satellites, de peur d'anéantir la galaxie.
Ainsi, l'accent est surtout mis dans cette nouvelle version sur l'expérience utilisateur, l'ergonomie et la gestion de projet, et moins sur des fonctionnalités de développement "de fond" (je ne parle pas ici de la plateforme .Net et de ses langages qui ont bien évolué avec la version 4.5 du framework).

Toutefois, l'intégration d'un test runner potable fait bien progresser l'IDE dans le domaine des tests unitaires. Elle rend Visual Studio "nu" enfin utilisable de façon à peu près correcte dans un contexte de développement agile piloté par les tests - il n'en demeure pas moins que si l'on veut une bonne productivité, un plugin tiers comme Resharper me parait encore indispensable.

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

lundi 2 janvier 2012

Podcasts

Ayant fait pas mal de déplacements fin 2011, j'ai occupé mon temps notamment en écoutant divers podcasts sur le développement logiciel. Pour tous ceux qui prévoient de passer du temps dans les transports cette année, je vous propose ma sélection avec mes 5 podcasts favoris en 2011 :

1. .NET Rocks! : un show très sémillant à l'américaine autour de .NET et des technologies Microsoft animé par Carl Franklin et Richard Campbell. Le podcast est de qualité professionnelle avec un très bon son, l'inconvénient étant les pubs. Bons invités (récemment Corey Haines) et toujours à la pointe de l'actualité et des sujets qui buzzent. Je le mets en premier pour la fréquence soutenue des épisodes qui fait qu'on a toujours quelque chose d'intéressant à se mettre sous la dent.

2. Visual Studio Talk Show : encore du .NET, mais comme son nom ne l'indique pas c'est un des rares podcasts francophones que j'aie trouvé sur le développement. Il est animé par les excellents Québecois Guy Barrette et Mario Cardinal. J'aime bien ce podcast pour son approche didactique et accessible, la variété des sujets et les acteurs de la communauté (québécois mais aussi français) invités.

3. Software Engineering Radio : un podcast généraliste sur le développement, les méthodes agiles... avec des invités de marque : Jurgen Appello, Lisa Crispin, Uncle Bob, Rich Hickey (créateur du langage Clojure) ont été reçus par le passé, sans compter un épisode assez mémorable avec Kent Beck.

4. Hanselminutes : le podcast de Scott Hanselmann assez orienté Web et Microsoft. J'aime bien le ton sérieux et toujours posé de Scott qui est un peu à l'inverse des compères de .NET Rocks!

5. The Java Posse : le podcast le plus populaire sur Java, avec son générique culte et sa bande de joyeux lurons assez marrants à écouter débattre en long et en large sur plein de sujets (spécial longs trajets donc). Et ça ne fait pas de mal de prendre des nouvelles de ce qui se fait du côté de Java de temps en temps ;)

Une mention également à The Agile Toolkit, Deep Fried Bytes et The Pragmatic Podcast qui valent le détour malgré une parution plus épisodique.

Je suis aussi preneur de vos suggestions de podcasts, notamment en français !


Sur ce, je vous souhaite une très bonne année 2012 :)

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

mercredi 2 juin 2010

Persistez votre domaine avec Raven DB

 

RavensIl y a peu, je vous parlais d'une possible synergie entre les bases de données NoSQL de type document et les Agrégats de Domain Driven Design.

Entretemps est sortie la base NoSQL pour la plateforme .NET d'Ayende Rahien, Raven DB, et elle confirme ces suppositions. Voici un extrait de la documentation de la base au corbeau :

« When thinking about using Raven to persist entities, we need to consider the two previous points. The suggested approach is to follow the Aggregate pattern from the Domain Driven Design book. An Aggregate Root contains several entities and value types and controls all access to the objects contained in its boundaries. External references may only refer to the Aggregate Root, never to one of its child objects.
When you apply this sort of thinking to a document database, there is a natural and easy to follow correlation between an Aggregate Root (in DDD terms) and a document in Raven. An Aggregate Root, and all the objects that it holds, is a document in Raven. »

Avec Raven, les données sont stockées dans des documents JSON qui ressemblent à ça :


Document

Contrairement à une base relationnelle dans laquelle les données résident dans des tables fragmentées qu'on peut relier par l'intégrité référentielle, il est possible de stocker dans ce type de document tout un graphe d'objets complexes et typés. Les préconisations pour la conception de documents Raven sont donc à l'inverse de celles qui président au design d'une base de données relationnelle : il s'agit de regrouper au sein d'un seul document les objets formant un tout logique. Par exemple, un Livre regroupe à la fois un titre mais aussi les objets que sont son Auteur et sa Catégorie. Il se trouve que la notion d'Agrégat et de Racine d'Agrégat de DDD recoupe en tous points ce concept.

Un des bénéfices qu'on constate immédiatement avec Raven DB est l'élimination pure et simple de la couche de mapping objet-relationnel, parfois source de bien des complications. En effet avec Raven, plus de défaut d'impédance entre les objets de l'application et leur équivalent persisté relationnel. La couche persistance s'en trouve bien allégée.


Exemple en C#

Comme un bon exemple vaut tous les discours, voici comment on peut implémenter un Entrepôt avec Raven et son client C# natif :


   1:      public class Livre
   2:      {
   3:          public string Id { get; set; }
   4:          public Auteur Auteur { get; set; }
   5:          public string Titre { get; set; }
   6:          public Categorie Categorie { get; set; }
   7:      }


   1:      public class EntrepotLivres
   2:      {
   3:          public EntrepotLivres(IDocumentSession documentSession)
   4:          {
   5:              session = documentSession;
   6:          }
   7:   
   8:          public void AjouterLivre(Livre livre)
   9:          {
  10:              session.Store(livre);
  11:              session.SaveChanges();
  12:          }
  13:   
  14:          public void SupprimerLivre(Livre livre)
  15:          {
  16:              session.Delete(livre);
  17:              session.SaveChanges();
  18:          }
  19:   
  20:          public Livre GetLivreParId(string id)
  21:          {
  22:              return session.Load<Livre>(id);
  23:          }
  24:   
  25:          private IDocumentSession session;
  26:      }


   1:  class Program
   2:  {
   3:      static void Main(string[] args)
   4:      {
   5:          var documentStore = new DocumentStore { Url = "http://localhost:8080" };
   6:          documentStore.Initialize();
   7:   
   8:          using (var session = documentStore.OpenSession())
   9:          {
  10:              EntrepotLivres entrepotLivres = new EntrepotLivres(session);
  11:              ...
  12:          }
  13:      }
  14:  }

Pas de fichier de mapping à gérer à côté de ça, l'entité Livre et tout ce qu'elle contient se persistera de façon immédiate... Simple, non ?

On remarque qu'on manipule une Session Raven. Il s'agit d'une implémentation du pattern Unit of Work assez similaire à ce qu'on trouve dans des frameworks d'ORM comme NHibernate. En fait, bon nombre de concepts et dénominations dans l'API client C# de Raven sont directement issus de NHibernate.


Index et requêtes

Pour l'instant, notre Entrepôt ne fait qu'ajouter des entités, les supprimer et les hydrater à partir de leur ID, ce qui se fait nativement avec Raven. Pour exécuter des requêtes plus complexes sur nos documents, il faut passer par des index. Les index sont des vues stockées sur disque semblables aux vues matérialisées des SGBDR. Ils sont taillés pour un type de recherche particulier.

Voici comment on définit en C# un index qui porte sur les catégories des livres :


   1:          documentStore.DatabaseCommands.PutIndex("LivresParCategorie", 
   2:              new IndexDefinition<Livre>
   3:              {
   4:                  Map = livres => from livre in livres
   5:                                  select new { livre.Categorie }
   6:              });

Cet index ne doit être créé qu'une fois, il peut aussi être défini via l'interface d'administration de Raven en utilisant la même syntaxe.

On peut maintenant ajouter une méthode à notre entrepôt qui retourne tous les livres d'une catégorie. Elle effectue une requête LuceneQuery sur l'index précédemment défini :


   1:          public IList<Livre> GetLivresParCategorie(Categorie categorie)
   2:          {
   3:              return session.LuceneQuery<Livre>("LivresParCategorie")
   4:                  .Where(l => l.Categorie == categorie)
   5:                  .ToList();
   6:          }

Il existe une autre technique d'indexation plus sophistiquée, map/reduce, dont je parlerai peut-être dans un autre billet.


Raven DB

Impressions


Ce que j'aime dans Raven DB :

  • Simplicité de persistance des objets et affranchissement complet de la couche d'ORM.
  • Les données sont accessibles sous forme RESTful (chaque document dispose d'une URL) et lisible par un humain.
  • Sans doute bien plus facilement scalable qu'un SGBDR du fait de la nature atomique et autonome d'un document.
  • API .NET et requêtage sur les indexes en Linq.
  • Raven se marie bien avec DDD et une partie de l'effort de design de la base est déjà fait si on a découpé ses Agrégats.

Ce qui me plait moins :

  • Pour exploiter pleinement Raven en termes de performances, il faut idéalement ramener un seul document de la base et que celui-ci contienne tout ce dont on a besoin. Cela peut mener à une tendance à calquer les documents sur les IHM.
  • Raven DB a peut-être un petit impact sur la persistance ignorance de notre domaine. Il semble qu'une entité qui fait référence à la racine d'un autre agrégat (donc située dans un autre document, en termes Raven) ne peut pas avoir une référence directe à cet objet mais est obligé de contenir son ID à la place. Dans notre exemple, l'Auteur d'un Livre est un objet entièrement contenu dans l'agrégat Livre parce qu'on ne stocke que son nom et prénom. Mais si l'Auteur devait faire l'objet d'un agrégat séparé à lui (par exemple si l'auteur est un Utilisateur du système), le Livre devrait alors contenir la clé de l'Auteur et plus l'Auteur lui même. Sachant que les IDs natifs de Raven sont très typiques ("auteurs/465" par exemple), on se retrouve avec une trace de Raven dans le graphe d'objets du domaine, et aussi la nécessité de passer par un entrepôt pour réhydrater l'objet dont on n'a que la clé.

Les doutes à lever à l'avenir :

  • Les performances. Ayende a publié des mesures de perfs prometteuses mais il va falloir qu'elles soient confirmées sur des projets à plus grande échelle. En particulier, il serait intéressant de voir comment le système de concurrence optimiste de Raven se comporte dans un contexte transactionnel intensif.
  • L'adoption. Je pense que les bases NoSQL ne survivront pas sans un écosystème solide à la fois en termes de communauté et d'outillage disponible. Si on ne peut pas faire avec les bases non relationnelles tout ce qu'on fait avec les SGBDR (monitoring, tuning, reporting, analyse de données...), elles resteront une bonne idée sur le papier mais un choix pauvre sur le terrain.

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 ?

jeudi 22 octobre 2009

Impressions sur Visual Studio 2010

La béta 2 de Visual Studio 2010 étant sortie il y a peu, il était grand temps pour moi de voir ce que cette nouvelle mouture de l'IDE de Microsoft avait sous le capot.

Un tour sur la page de download de la béta, et après un processus d'installation classique mais efficace, la nouvelle interface s'offre à nos yeux.

Visual Studio 2010

Bon, à part une couleur bleu horizon du plus bel effet, on ne peut pas dire que la page de démarrage soit une révolution par rapport à Visual Studio 2008. Nouveau/Ouvrir Projet, Projets récents, quelques nouveaux liens comme la personnalisation de la page d'accueil, les habitués ne seront pas dépaysés.

Lorsqu'on ouvre un solution et qu'on commence à jouer avec la bête, petite déception : l'interface ne semble pas très réactive. Même si c'est pardonnable pour une béta, on constate à l'usage beaucoup de lenteurs et même quelques freezes momentanés (machine sous Windows Vista, Core 2 Duo 2 GHz, 4 Go de RAM). Peut-être la faute à l'IHM basée sur du WPF.

Mais voyons maintenant les principales nouveautés autour de ce qui nous intéresse le plus : la manipulation du code.


Navigation

Quick Search

Les développeurs de Visual 2010 avaient annoncé une navigabilité dans le code grandement améliorée, et c'est certainement un des domaines où le plus de progrès ont été faits. Voici les avancées les plus marquantes :

  • Quick Search : activée par un Ctrl-virgule, cette boite de dialogue permet une recherche intuitive avec suggestion de résultat lorsqu'on tape les premières lettres d'une classe ou les majuscules qui composent son nom. En fait, un passage incontournable pour naviguer entre vos fichiers/classes, tellement il est pratique.
  • Surlignage de références : lorsque le curseur se trouve sur une variable, méthode ou type, après quelques instants celle-ci est discrètement surlignée ainsi que toutes ses occurrences dans le fichier actif. Une aide visuelle non négligeable qui évite de chercher les références manuellement.
  • Hiérarchie des appels : cette nouvelle fonctionnalité affiche un arbre contenant les appels à la méthode/property sélectionnée, les appels au code qui l'appellent, et ainsi de suite. Cela évite de faire des "find usages" successifs lorsqu'on veut remonter une pile d'appels dans le code.


Refactoring

Smart tag

Au système d'affichage d'erreurs à la volée déjà existant, les développeurs de Visual Studio 2010 ont ajouté, exactement à la manière d'un ReSharper, des petites boîtes de suggestion (les SmartTags) qui lorsqu'elles sont déroulées proposent un menu avec diverses actions de refactoring et de génération de code.

Si certaines sont utiles en toute occasion (implémentation/héritage automatique, suggestion d'ajout de références...), d'autres sont clairement orientées TDD comme la génération de stubs de classe ou de méthodes qui n'existent pas encore.
Le raccourci pour les SmartTags est Ctrl-point-virgule.


Un tueur de ReSharper ? Pas vraiment

VS2010-ReSharper

Au vu de ces éléments, on pourrait penser que Microsoft a encore fait ce qu'il sait le mieux faire, c'est à dire copier les innovations de plus petits acteurs du marché pour les intégrer et les généraliser dans ses mastodontes. Le petit poucet innovant étant dans le cas présent JetBrains avec son ReSharper, plug-in tellement utile à la vie du développeur (il comportait déjà bon nombre des fonctionnalités de VS 2010 citées ci-dessus dès ses premières versions) qu'on avait du mal à s'en passer.
Le problème, c'est que Microsoft a copié cette recette à succès, mais en oubliant quelques ingrédients. Il manque à Visual 2010 tout un tas de petites fonctions de ReSharper qui me font dire que ce dernier sera toujours indispensable :

  • Pas de fermeture automatique des accolades
  • Pas d'autocomplétion des noms de variables avec un nom par défaut
  • Pas de go to Inheritor / base
  • Pas de surlignage des erreurs à la volée pour certains types d'erreurs (ex : non implémentation d'une interface)
  • SmartTags mal placés : par exemple sur le nom de la classe mère après les deux points d'un héritage, mais pas sur le nom de la classe fille donc peu visible
  • etc.

Au final, après quelques heures de manipulation de Visual Studio 2010, mon impression est qu'il n'est toujours pas aussi facile à piloter qu'un 2008 + ReSharper. Il manque cette réactivité, cette intuitivité qui font que l'IDE semble répondre au doigt et à l'oeil du développeur et lui apporte une productivité maximum.

Si l'on ajoute que JetBrains met la barre encore plus haut pour le futur ReSharper 5, je pense que le petit plug-in a encore de beaux jours devant lui...

samedi 12 septembre 2009

Retour sur .NET RIA Services

Il y a peu, nous avons décidé au sein de notre équipe de faire migrer un projet en cours de développement de Silverlight 2 vers Silverlight 3, ce dernier étant sorti en release officielle au courant de l'été. Contrairement à ce que nous craignions, l'opération s'est bien passée, notamment grâce à cette petite checklist bien utile. Une fois les tools Silverlight 3 installés, l'assistant de conversion de Visual Studio s'occupe d'à peu près tout, sauf quelques mises à jour à faire si vous utilisez des services WCF.

Silverlight

Mais une autre question s'est posée à nous : fallait-il transformer nos services WCF Silverlight 2 (assez lourds à manipuler et à mettre à jour) en RIA Services ? En effet ce nouveau framework applicatif paraissait contenir plein de bonnes surprises pour nous faciliter la vie. Utilisant pour prendre la décision une méthode un peu débile mais qui marche, à savoir un tableau avec une colonne + et une colonne -, voici ce que nous avons obtenu :

Du côté des avantages :

+ Un découpage clair des couches et un système de DomainContext qui peut se marier avec du MVVM, du MVC ou même du MVP (solution que nous avions retenue).

+ Les classes clientes des services côté Silverlight sont automatiquement générées lors du build des services, pas de référence de service à ajouter ou mettre à jour contrairement à du WCF classique. Lors du passage sur un serveur d'intégration continue ou de recette, on s'ôte aussi un gros poids puisque la mise à jour des références vers le nouvel emplacement des services n'a pas lieu d'être.

+ Tout un tas de facilités introduites aussi bien au niveau de l'interrogation des services que de la mise à jour des entités.

+ Exceptions de services WCF mieux gérées qu'avec Silverlight 2 (le retour d'exceptions du service vers le client Silverlight n'était tout simplement pas géré) et Silverlight 3 (où il faut surcharger un comportement pour générer le bon code d'erreur à renvoyer au client).

+ Meilleure prise en charge des rôles et droits utilisateur...

Les inconvénients :

- RIA Services est encore en CTP, pour une sortie pas prévue avant 2010 (!)

- Il faut une petite bidouille pour installer RIA Services sur un Visual Studio français.

- Une testabilité ... à tester. Le mocking des DomainServices n'a pas l'air évident même si Nikhil Kothari a proposé une solution sur son blog.

- Mauvaise intégration avec ReSharper : il faut inclure les classes clientes générées par RIA Services dans le projet Visual Studio sinon ReSharper ne retrouve pas ses petits.

Tout compte fait, ça a beau ne pas être une version finale, les avantages et les gains de temps induits sont tels qu'on ne voit pas comment trouver le courage de s'en passer ! Reste le problème de la stabilité d'une CTP et de la nécessité de repenser les services, qui peut, on le comprend, en rebuter certains. Toutefois, dans un projet très peu critique comme le nôtre, le risque semble en valoir la chandelle.