blog.pagesd.info

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

vendredi 30 octobre 2009

Test-Driven Development avec ASP.NET MVC (fin)

Cette fois je termine cette 6° partie du tutoriel de Gestion des Contacts. En un, il me reste pas grand chose à faire. En deux, j'utilise les grands moyens et je lance deux Visual Web Developper, un avec ma solution en cours de finition et un avec la solution "finie" de l'étape 6 du tutoriel.

Si je regarde au niveau du contrôleur des contacts, je constate que l'action Index a pas mal changé par rapport à ce qui existait avant. On est passé d'une seule ligne :

public ActionResult Index()
{
    return View(_service.ListContacts());
}

A tout ça :

public ActionResult Index(int? id)
{
    var model = new IndexModel
    {
        Groups = _service.ListGroups(),
        SelectedGroup = _service.GetGroup(id)
    };

    if (model.SelectedGroup == null)
        return RedirectToAction("Index", "Group");

    return View(model);
}

Si je tente de comprendre ce qui est fait :

  • on crée un objet de type IndexModel (à voir d'où il sort) et on défini sa propriété "Groups" avec la liste des groupes et sa propriété SelectedGroup avec un objet Group correspondant à l'identifiant passé en paramètre.
  • s'il n'y de groupe sélectionné, le contrôleur redirige l'utilisateur vers la gestion des groupes (soit vers l'action Index du contrôleur des groupes)
  • sinon, l'objet IndexModel est transmis à la vue Index pour affichage

Première étape, trouver à quoi correspond le type IndexModel. Il était bien planqué dans un sous-répertoire ViewData du dossier Models, mais j'ai fini par trouver à quoi ressemble IndexModel.cs :

using System.Collections.Generic;

namespace ContactManager.Models.ViewData
{
    public class IndexModel
    {
        public Group SelectedGroup { get; set; }
        public IEnumerable<Group> Groups { get; set; }
    }
}

C'est ni plus ni moins qu'une classe qui sert à contenir les 2 objets attendus par la nouvelle vue Contacts\Index.aspx :

  • la liste des groupes qui apparaitra dans le <ul id="leftColumn">
  • la liste des contacts qui sera rendue dans le <div id="divContactList">

Je fais pareil mais ça ne compile pas car la méthode GetGroup() attend un paramètre de type "int" alors que l'action Index ne peut que lui envoyer un paramètre de type "int?". Il me faut donc modifier la méthode GetGroup() du service et par conséquent celle du repository pour qu'elles gèrent un paramètre "int?" (sans oublier les interfaces et le FakeContactManagerRepository.cs).

En fait non. Il faut seulement modifier le GetGroup() du service pour qu'il prenne en compte le fait que le paramètre "id" peut être null :

public Group GetGroup(int? id)
{
    if (id.HasValue)
        return _repository.GetGroup(id.Value);
    return _repository.GetFirstGroup();
}

Ce coup-ci ça compile, mais ça ne s'exécute toujours pas :(

c:\MVC\ContactManager\ContactManager\Views\Contact\Index.aspx(13): error CS1061: 'System.Web.Mvc.HtmlHelper<ContactManager.Models.ViewData.IndexModel>' ne contient pas une définition pour 'Selected' et aucune méthode d'extension 'Selected' acceptant un premier argument de type 'System.Web.Mvc.HtmlHelper<ContactManager.Models.ViewData.IndexModel>' n'a été trouvée (une directive using ou une référence d'assembly est-elle manquante ?)

D'où qu'il sort ce "Selected" ? Normalement il ne devrait y avoir que des "SelectedGroup" dans Contacts\Index.aspx. Ou alors c'est pas ma faute parce que j'ai copié / collé du tutoriel. Je regarde quand même et que vois-je ?

<li <%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>

Oh! Un Html Helper. Voyons-voir s'il y a du nouveau dans le dossier Helpers ? Oh! un fichier SelectedHelper.cs

using System;
using System.Web.Mvc;

namespace Helpers
{
    public static class SelectedHelper
    {

        public static string Selected<T>(this HtmlHelper helper, T value1, T value2)
        {
            if (value1.Equals(value2))
                return "class=\"selected\"";
            return String.Empty;
        }
    }
}

J'ajoute ce fichier à mon projet, je recompile et j'exécute mais j'ai toujours une erreur.

c:\MVC\ContactManager\ContactManager\Views\Contact\Index.aspx(14): error CS1061: 'System.Web.Mvc.HtmlHelper<ContactManager.Models.ViewData.IndexModel>' ne contient pas une définition pour 'Selected' et aucune méthode d'extension 'Selected' acceptant un premier argument de type 'System.Web.Mvc.HtmlHelper<ContactManager.Models.ViewData.IndexModel>' n'a été trouvée (une directive using ou une référence d'assembly est-elle manquante ?)

Ok, j'ai oublié le <%@ Import Namespace="Helpers" %> dans Index.aspx. Je recompile, j'exécute et ça marche !

Je vais enfin pouvoir passer à la 7° partie du tutoriel et faire de l'Ajax. Mais là tout de suite, c'est le week-end !


Billet suivant dans la série : Utiliser Ajax avec ASP.NET MVC

jeudi 29 octobre 2009

Test-Driven Development avec ASP.NET MVC (suite)

Je dois avouer que j'ai pas mal dévié de l'objectif de cette 6° partie du tutoriel de Gestion des Contacts. Son but était d'illustrer l'intérêt du test-driven-development en prenant pour exemple l'ajout d'une notion de groupes de contacts. Comme je trouvais que la fin du tutoriel passait un peu vite sur la réalisation concrète des modifications réalisées et que je ne voulais pas me contenter de les récupérer toutes faites, j'ai pris ça comme un exercice pour compléter mon apprentissage d'ASP.NET MVC.

Première tentative

Dans ce billet, je vais donc continuer la modification de la base de données, c'est à dire ajouter une colonne groupId à la table des contacts et définir une relation entre les deux tables Contacts et Groups.

Je commence par l'ajout de la colonne groupId de type int, en prenant soin de définir sa propriété "Default Value or Binding" à 1. Cette petite astuce m'évite d'avoir à vider la table des contacts lorsque j'ajoute la relation entre cette nouvelle colonne et la table Groups créée la fois précédente.

Puis je m'occupe de la mise à jour du modèle. Mais ça ne se fait pas tout seul :( Je dois passer par la toolbox pour ajouter l'association entre les deux tables. Et même après ça, j'ai des erreurs de compilation ! je recommence, je bidouille, mais rien à faire, ça ne veut pas compiler...

Seconde tentative

Pour aller plus vite, je récupère la base de données (App_Data\ContactManagerDB*.*) et le modèle (Models\ContactManagerModel.*) qui datent d'avant l'ajout de la table "Groups" et je refais tout comme indiqué dans le tutorial (ou à peu près).

  • ajout de la table "Groups"
  • création d'un premier groupe "Business"
  • ajout de la colonne "groupId" à la table "Contacts", en mettant 1 comme "Default Value or Binding"
  • définition de la relation entre les deux tables
  • mise à jour du modèle

Et cette fois-ci, tout marche comme sur des roulettes ! Je vérifie que ça compile (Ok), je quitte VWD et je sauvegarde. On ne sait jamais...

Prise en compte de groupId dans la table Contacts

Pour la suite, je vais déjà tenter de faire apparaitre la liste des groupes dans les formulaires de création et de mise à jour des contacts, mais juste pour l'affichage, sans chercher à en tenir compte pour enregistrer les contacts.

Je commence par modifier la classe EntityContactManagerRepository.cs pour modifier les requêtes LINQ et leur ajouter les .Include() afin de gérer la relation entre les tables :

...
public Contact GetContact(int id)
{
    return (from c in _entities.ContactSet.Include("Group")
            where c.Id == id
            select c).FirstOrDefault();
}
...
public Group GetGroup(int id)
{
    return (from g in _entities.GroupSet.Include("Contacts")
            where g.Id == id
                    select g).FirstOrDefault();
}
...

J'exécute et je teste l'ajout d'un groupe "Friends" et ça passe. J'essaie alors de créer un nouveau contact mais là le clic sur le bouton "Create" n'a plus aucun effet. Un petit débugage m'apprend que _entities.SaveChanges() ne fonctionne plus.

Qu'à cela ne tienne, je fais une autre modification au repository pour associer le contact à créer au groupe par défaut juste avant de procéder à son enregistrement et là ça marche.

public Contact CreateContact(Contact contactToCreate)
{
    // Associate group with contact
    contactToCreate.Group = GetGroup(1);

    // Save new contact
    _entities.AddToContactSet(contactToCreate);
    _entities.SaveChanges();
    return contactToCreate;
}

Et ça marche même pour la mise à jour sans qu'il n'y ait rien d'autre à faire. Tant mieux.

Bon. Aux vues maintenant.

Mise à jour des vues contacts pour lister les groupes

Je démarre avec la vue Create.aspx en regardant comment c'est fait dans les sources finis de la 6° partie et en refaisant la même chose pas à pas.

Ajouter une dropdown list des groupes dans le formulaire Views\Contact\Create/aspx :

<p>
    <label for="GroupId">Group:</label>
    <%= Html.DropDownList("GroupId") %>
</p>

Tel quel, ça reste insuffisant et on obtient une erreur «There is no ViewData item with the key 'GroupId' of type 'IEnumerable<SelectListItem>'.» quand on essaie d'afficher le formulaire de création d'un contact.

Il faut donc que le contrôleur fasse passer la liste des groupes à la vue :

public ActionResult Create()
{
    if (!AddGroupsToViewData(-1))
        return RedirectToAction("Index", "Group");
    return View("Create");
}

protected bool AddGroupsToViewData(int selectedId)
{
    var groups = _service.ListGroups();
    ViewData["GroupId"] = new SelectList(groups, "Id", "Name", selectedId);
    return groups.Count() > 0;
}

La fonction AddGroupsToViewData() ajoute la liste des groupes à la collection ViewData et dans le cas où cette collection serait vide, le contrôleur redirige l'utilisateur vers la liste des groupes afin qu'il puisse y créer au moins un nouveau groupe.

Pour que cela fonctionne, il faut encore ajouter using System.Linq;, sinon la syntaxe groups.Count() n'est pas acceptée.

Après avoir vérifié que ça marche bien, j'en ai profité pour présenter les noms des groupes dans l'ordre alphabétique. Après pas mal de Google, j'ai trouvé comment faire :

public IEnumerable<Group> ListGroups()
{
    return _entities.GroupSet.OrderBy(o => o.Name).ToList();
}

C'est quand même plus joli.

Je continue donc avec la vue Edit.aspx où je refais quasiment la même chose.

Ca avance ! Je peux maintenant associer un contact au groupe de contacts de mon choix. Y'a plus qu'à faire en sorte que ce choix soit enregistré dans la base de données.

Mais avant, je ne résiste pas au plaisir de trier aussi la liste des contacts :

public IEnumerable<Contact> ListContacts()
{
    return _entities.ContactSet.OrderBy(o => o.FirstName).ThenBy(o => o.LastName).ToList();
}

Utilisation du groupe sélectionné pour créer un contact

Maintenant, je vais essayer de tenir compte du groupe sélectionné lors de la création d'un contact.

Pour cela, il faut théoriquement :

  • récupérer l'identifiant du groupe sélectionné dans le formulaire
  • utiliser cet identifiant au moment de l'enregistrement du contact, en lieu et place du "1" que j'avais mis en dur

Facile ! Si je regarde le code fini de la 6° partie, un paramètre int groupId a été ajouté à l'action Create de ContactController.cs.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(int groupId, [Bind(Exclude = "Id")] Contact contactToCreate)
{
    if (_service.CreateContact(contactToCreate))
        return RedirectToAction("Index");
    AddGroupsToViewData(-1);
    return View("Create");
}

Et pour que ça marche, il faut aussi modifier ContactControllerTest.cs qui teste cette action en donnant la valeur 1 à ce nouveau paramètre.

Donc pour l'instant le contrôleur récupère l'identifiant du groupe qui a été choisi. Il faut ensuite trouver comment faire suivre cet identifiant au repository pour qu'il en tienne compte. J'aurai eu tendance à écrire contactToCreate.groupId = groupId, mais malheureusement l'objet Contact n'a pas de propriété groupId.

Il faut donc procéder pas à pas et commencer par faire passer le groupId du contrôleur au service : _service.CreateContact(contactToCreate) devient _service.CreateContact(groupId, contactToCreate), ce qui implique de modifier IContactManagerService.cs, ContactManagerService.cs et ContactManagerServiceTest.cs

Puis ensuite faire suivre le groupId du service au repository, ce qui nécessite la mise à jour de IContactManagerRepository.cs, EntityContactManagerRepository.cs et FakeContactManagerRepository.cs

1. ContactController.cs récupère groupId de la vue et l'envoie au service

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(int groupId, [Bind(Exclude = "Id")] Contact contactToCreate)
{
    if (_service.CreateContact(groupId, contactToCreate))
        return RedirectToAction("Index");
    AddGroupsToViewData(-1);
    return View("Create");
}

2. ContactManagerService.cs récupère groupId du contrôleur et l'envoie au repository

public bool CreateContact(int groupId, Contact contactToCreate)
{
    // Validation logic
    if (!ValidateContact(contactToCreate))
        return false;

    // Database logic
    try
    {
        _repository.CreateContact(groupId, contactToCreate);
    }
    catch
    {
        return false;
    }
    return true;
}

3. EntityContactManagerRepository.cs : récupère groupId du service et l'utilise pour créer le contact

public Contact CreateContact(int groupId, Contact contactToCreate)
{
    // Associate group with contact
    contactToCreate.Group = GetGroup(groupId);

    // Save new contact
    _entities.AddToContactSet(contactToCreate);
    _entities.SaveChanges();
    return contactToCreate;
}

Je lance l'application et crée un nouveau contact après avoir sélectionné "Friends" dans la dropdown list des groupes, je clique sur le bouton "Create" et ça marche ! Faut vérifier dans la table Contacts pour le croire mais c'est bien le cas.

Utilisation du groupe pour modifier un contact

Ca consiste très simplement à refaire à peu près pareil avec la vue Edit.aspx et les méthode Edit() qu'on trouve un peu partout dans le contrôleur, le service et le repository.

Déjà il faut faire en sorte que la vue Edit.aspx fasse apparaitre correctement le groupe auquel le contact a été rattaché. Pour cela, il suffit que le contrôleur lui donne la bonne information :

public ActionResult Edit(int id)
{
    var contactToEdit = _service.GetContact(id);
    AddGroupsToViewData(contactToEdit.Group.Id);
    return View(contactToEdit);
}

Au moins maintenant quand je vais en modification du contact que je viens de créer, c'est bien le "Friends" qui apparait sélectionné dans la dropdown list des groupes.

Reste plus qu'à refaire les 3 étapes pour que le groupe sélectionné au niveau de la vue Edit.aspx soit transmis jusqu'au niveau du repository.

1. ContactController.cs récupère groupId de la vue et l'envoie au service

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int groupId, Contact contactToEdit)
{
    if (_service.EditContact(groupId, contactToEdit))
        return RedirectToAction("Index");
    AddGroupsToViewData(groupId);
    return View("Edit");
}

2. ContactManagerService.cs récupère groupId du contrôleur et l'envoie au repository

public bool EditContact(int groupId, Contact contactToEdit)
{
    // Validation logic
    if (!ValidateContact(contactToEdit))
        return false;

    // Database logic
    try
    {
        _repository.EditContact(groupId, contactToEdit);
    }
    catch
    {
        return false;
    }
    return true;
}

3. EntityContactManagerRepository.cs : récupère groupId du service et l'utilise pour mettre à jour le contact

public Contact EditContact(int groupId, Contact contactToEdit)
{
    // Get original contact
    var originalContact = GetContact(contactToEdit.Id);
    
    // Update with new group
    originalContact.Group = GetGroup(groupId);
    
    // Save changes
    _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
    _entities.SaveChanges();
    return contactToEdit;
}

Quel progrès ! Je peux enfin rattacher un contact à un groupe au moment de sa création et même en changer lors de sa modification, et tout ça pour de vrai. Et sans compter que les tests unitaires sont toujours au vert.

Je touche au but

Pour enfin arriver au bout de cette 6° étape, il me reste juste à faire apparaitre les groupes au niveau de la vue Contact\Index.aspx qui affiche la liste des groupes. Malheureusement pour moi, le tutoriel n'a pas choisi la facilité en se contentant d'ajouter une colonne "Group" à cette liste mais a préféré filtrer la liste par groupe.

Juste pour le fun, je commence par essayer d'ajouter une colonne "Group" à la liste en modifiant Contact\Index.aspx :

<td>
    <%= Html.Encode(item.Group.Name) %>
</td>

Mais à l'exécution, ça ne passe pas et j'ai une erreur «La référence d'objet n'est pas définie à une instance d'un objet.» à cause de item.Group.Name. Je fais des progrès parce que je comprend de suite que la liste des contacts que le contrôleur ContactController.cs fait passer à la vue n'a pas été modifiée pour y faire apparaitre le groupe.

Une très légère modification à EntityContactManagerRepository.cs et c'est résolu :

public IEnumerable<Contact> ListContacts()
{
    return _entities.ContactSet.Include("Group").OrderBy(o => o.FirstName).ThenBy(o => o.LastName).ToList();
}

J'en serais bien resté là, mais la septième (et dernière !) étape du tutoriel va modifier cette vue Index pour utiliser Ajax. Si je veux vraiment aller jusqu'au bout de ce tutoriel il va falloir que j'en passe par le filtrage des contacts en fonction du groupe...

Je supprime donc mes modifications et c'est reparti.

Pour ce qui concerne la vue, je fais du copier / coller depuis le résultat du tutoriel, parce que c'est un peu tard et que je n'ai plus le courage de tout faire moi même. De toute façon, le principe est assez simple. On commence par faire deux blocs dans le html, le premier avec la liste des groupes (c'est le <ul id="leftColumn">) et le second avec la table des contacts (le <div id="divContactList">).

<ul id="leftColumn">
<% foreach (var item in Model.Groups) { %>
    <li <%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>
    <%= Html.ActionLink(item.Name, "Index", new { id = item.Id }) %>
    </li>
<% } %>
</ul>

<div id="divContactList">
    <table class="data-table" cellpadding="0" cellspacing="0">
        <thead>
            <tr>
                ...
            </tr>
        </thead>
        <tbody>
            <% foreach (var item in Model.SelectedGroup.Contacts)
               { %>
            <tr>
                ...
            </tr>
            <% } %>
        </tbody>
    </table>
</div>    

Puis on ajoute un peu de CSS pour faire flotter tout ça que les deux blocs apparaissent côte à côte.

F5 pour voir ce que ça donne... et c'est la grosse erreur :

c:\MVC\ContactManager\ContactManager\Views\Contact\Index.aspx(12): error CS1061: 'System.Collections.Generic.IEnumerable<ContactManager.Models.Contact>' ne contient pas une définition pour 'Groups' et aucune méthode d'extension 'Groups' acceptant un premier argument de type 'System.Collections.Generic.IEnumerable<ContactManager.Models.Contact>' n'a été trouvée (une directive using ou une référence d'assembly est-elle manquante ?)

Comme maintenant je commence à être doué, je vois très vite que la vue Index hérite toujours de System.Web.Mvc.ViewPage<IEnumerable<ContactManager.Models.Contact>> et que par conséquent il n'y a rien pour remplir la liste des groupes. Zut alors, il fallait aussi que je copie / colle la première ligne de l'Index.aspx du tutoriel.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ContactManager.Models.ViewData.IndexModel>" %>

Attendez-voir. Y'a un IndexModel. Il sort d'où lui ?

Je relance l'application pour la forme, mais je sais bien que ça va planter.

c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\01b85515\43ff20e2\App_Web_index.aspx.ebd9867.w7_tnpa3.0.cs(148): error CS0234: Le type ou le nom d'espace de noms 'ViewData' n'existe pas dans l'espace de noms 'ContactManager.Models' (une référence d'assembly est-elle manquante ?)

Finalement, c'est pas ce soir que je vais finir...


Billet suivant dans la série : Test-Driven Development avec ASP.NET MVC (fin)

mardi 20 octobre 2009

Test-Driven Development avec ASP.NET MVC (suite)

Enfin, ça y est, je vais enfin faire des trucs pour de vrai : modifier la base de données, écrire du code qui permet à l'utilisateur de réellement gérer des groupes de contacts... et venir à bout de la 6° partie du tutoriel pour gérer des contacts.

Mises à jour comme dans le tutoriel

La base de données : facile

Pour commencer, direction le "Database Explorer" pour modifier la base de données :

  • création d'une table "Groups" pour enregistrer les groupes de contacts
  • ajout d'une colonne "GroupId" à la table "Contacts" pour y stocker l'identifiant du groupe auquel est rattaché le contact
  • définition de la relation entre les tables "Contacts" et "Groups"

Le modèle : encore plus facile

Rien à ajouter au tutoriel.

Le Repositiry : ça se complique

Zut ! Le tutoriel donne bien les modifications à apporter repository (l'interface et la classe), mais pas un seul mot sur les autres modifications nécessaires...

  • Au début de cette 6° partie, on avait créé une classe Group à la main puisqu'on n'avait pas encore touché à la base de données. Ce coup-ci, pour que ça marche il faut la supprimer sinon on ne peut pas compiler parce que Missing partial modifier on declaration of type 'ContactManager.Models.Group'; another partial declaration of this type exists.
  • Les méthodes CreateContact() et EditContact() du repository attendent désormais un premier paramètre int groupId => il faut donc modifier le service ContactManagerService pour que celui-ci le leur fasse passer !
  • Qu'à cela ne tienne, un rapide coup d'oeil sur sur les sources finis de la 6° partie me confirme qu'il faut aussi faire apparaitre ce paramètre groupId au niveau du service : dans l'appel aux méthodes du repository mais aussi dans les signatures des méthodes du service
  • Et bien sûr, ça ne compile toujours pas puisque le contrôleur ContactController ne fait pas passer ce paramètre groupId aux méthodes de ContactManagerService...

Stop ! J'efface tout et je recommence pas à pas. En un, gérer la table des groupes. En deux, modifier la gestion des contacts pour tenir compte des groupes.

Mises à jour à ma façon

Une fois revenu en arrière :

  • mise à jour de la base de données en créant seulement la table "Groups", sans toucher à la table "Contacts"
  • mise à jour du modèle pour prendre en compte la nouvelle table (et pas la relation entre les tables "Groups" et "Contacts" puisqu'elle n'a pas été créée)
  • suppression de Models/Group.cs
  • mise à jour du repository pour la partie qui concerne les opérations sur la table "Groups"
  • mise à jour du FaleRepository pour que ça compile
  • un coup de tests unitaires et tout continue à être bon.

Ajout de la vue Index

Ca avance bien, donc je continue :

  • création d'une vue Groups/Index en automatique
  • modification de site.master pour ajouter un onglet "Manage Contact Groups"
  • compilation => ça marche, mais la liste est vide (normal puisque la table "Groups" est vide)

Passage par le "Database Explorer" (Show Table Data) pour ajouter un premier groupe "Business" et là la liste ressemble enfin à quelque chose. Et toujours rien de cassé niveau tests unitaires !

D'après ce que je vois dans la copie d'écran du tutoriel, la liste des groupes est un peu "spéciale". Elle ne contient pas de bouton "Edit" mais seulement un lien image pour supprimer. Et pas de lien pour créer un nouveau groupe, mais directement une zone de texte pour saisir le libellé d'un nouveau groupe et un nouton "Create" pour l'enregistrer.

Donc retour sur la vue Index pour essayer d'arriver à la même chose. Pour commencer, de simples copier / coller depuis la vue Index des Contacts suffisent pour modifier la liste. C'est juste un peu plus compliqué pour ajouter les contrôles nécessaires pour la création d'un nouveau groupe, mais pas infaisable.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<ContactManager.Models.Group>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
        Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Index</h2>

    <%= Html.ValidationSummary() %>

    <table>
        <tr>
            <th class="actions delete">
                Delete
            </th>
            <th>
                Name
            </th>
        </tr>

    <% foreach (var item in Model) { %>
    
        <tr>
            <td class="actions delete">
                <a href='<%= Url.Action("Delete", new {id=item.Id}) %>'><img src="../../Content/Delete.png" alt="Delete" /></a>
            </td>
            <td>
                <%= Html.Encode(item.Name) %>
            </td>
        </tr>
    
    <% } %>

        <tr>
            <th>
                Add
            </th>
            <th>
                <%= Html.TextBox("Name") %>
                <input type="submit" value="Create" />
                <%= Html.ValidationMessage("Name", "*") %>
            </th>
        </tr>

    </table>

</asp:Content>

Et c'est OK pour l'interface utilisateur. Après avoir vérifié que le contrôleur a bien le code nécessaire pour gérer le Create, je compile, j'exécute et je tente de créer un nouveau groupe "Friends". Et ça ne marche pas :( J'ai beau m'acharner sur le bouton "Create", rien ne se passe...

Ok ! J'ai pas de balise <form> dans ma page ! Il faut penser à englober les contrôles <input> entre

<% using (Html.BeginForm("Create", "Group")) { %>

et

<% } %>

MVC, c'est pas encore automatique pour moi. Je recommence, resaisi "Friends", clique sur le bouton "Create" et boum !

Erreur du serveur dans l'application '/'.
--------------------------------------------------------------------------------
The view 'Create' or its master could not be found. The following locations were searched:
~/Views/Group/Create.aspx
~/Views/Group/Create.ascx
~/Views/Shared/Create.aspx
~/Views/Shared/Create.ascx 
Description : Une exception non gérée s'est produite au moment de l'exécution de la demande Web actuelle. Contrôlez la trace de la pile pour plus d'informations sur l'erreur et son origine dans le code. 

Quoi encore ? Pourquoi diable est-ce que ça veut afficher une vue Create ? J'ai rien demandé moi !

...

Ca y est, j'ai trouvé le coupable dans GroupController.cs :

public ActionResult Create(Group groupToCreate)
{
    if (_service.CreateGroup(groupToCreate))
        return RedirectToAction("Index");
    return View("Create");
}

Il y a un return View("Create"); alors que la création se fait dans la vue Index => à remplacer par return View("Index");.

Je re-lance, re-saisi "Friends", re-clique sur "Create" et re-boum. Mais ce coup-ci je me retrouve dans le débugueur avec une erreur «NullReferenceException was Unhandled by user code» sur la ligne <% foreach (var item in Model) { %>.

...

Ca va bien finir par marcher. Mais pour gagner du temps, je vais pomper sur le source de GroupController.cs du tutoriel où il y a un return View("Index", _service.ListGroups());. J'aurai pu trouver ! Dans l'action Index il y a déjà un return View(_service.ListGroups());. C'est sûr que si je renvoie la vue Index sans lui passer la liste des groupes, ça peut pas marcher...

Je re-lance, re-saisi "Friends", re-clique sur "Create" et voilà-t-y pas que ça me dit «A value is required.» (en rouge en plus). Je re-lance, re-saisi "Friends" (des fois que le coup d'avant j'ai oublié ou qu'il aurait pas bien compris), re-clique sur "Create" et pareil.

C'est parce que je suis idiot. Comment qu'elle fait mon action Create si je lui dit pas de récupérer les valeurs du POST pour fabriquer l'objet Group à insérer ? Je regarde comment fait le Create dans ContactController.cs, copie / colle et c'est parti :

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "Id")]Group groupToCreate)
{
    if (_service.CreateGroup(groupToCreate))
        return RedirectToAction("Index");
    return View("Index", _service.ListGroups());
}

Je re-lance, re-saisi "Friends", re-clique sur "Create" et euréka ! J'ai réussi à insérer un nouveau groupe !!!

Je re-lance, ne saisi rien dans la zone texte, clique sur le bouton "Create" et j'ai bien un message «Name is required.» pour m'apprendre à vouloir créer un groupe sans lui donner de nom.

Je re-lance, clique sur l'image "Delete" devant le groupe "Friends" et ça me sort une erreur «La ressource est introuvable.». Ah ouais, j'ai pas encore géré la suppression.

Ajout de la suppression d'un groupe

Avant d'attaquer ça, je refais vite un coup de tests unitaires pour vérifier que tout continue de fonctionner comme prévu et ouf! rien de cassé.

J'ajoute l'action Delete au contrôleur GroupController.cs, en faisant tout comme dans GroupContact.cs :

public ActionResult Delete(int id)
{
    var groupToDelete = _service.GetGroup(id);
    return View(groupToDelete);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(Contact groupToDelete)
{
    if (_service.DeleteGroup(groupToDelete))
        return RedirectToAction("Index");
    return View();
}

Un filet rouge sous _service.GetGroup et _service.DeleteGroup me rappelle que ces deux méthodes ne sont pas encore implémentées dans ContactManagerService.cs (et pas déclarées dans IContactManagerService.cs par la même occasion).

public bool DeleteGroup(Group groupToDelete)
{
    // Database logic
    try
    {
        _repository.DeleteGroup(groupToDelete);
    }
    catch
    {
        return false;
    }
    return true;
}

public Group GetGroup(int id)
{
    return _repository.GetGroup(id);
}

Il me reste alors à créer une vue Delete (strongly-typed view, empty content) puis à m'inspirer de la vue Contact/Delete.aspx pour la compléter :

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ContactManager.Models.Group>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
        Delete
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Delete</h2>

    <p>
    Are you sure that you want to delete the entry for
    <%= Model.Name %>
    </p>

    <% using (Html.BeginForm(new { Id = Model.Id }))
       { %>
       <p> 
            <input type="submit" value="Delete" /> &nbsp; <%=Html.ActionLink("Cancel", "Index") %>
        </p>
    <% } %>

</asp:Content>

Je lance, clique sur l'image "Delete" devant le groupe "Friends", clique sur le bouton "Delete" pour confirmer et plus de groupe "Friends". Trop fort, je peux aussi supprimer un groupe !!!

Je re-lance, clique sur l'image "Delete" devant le groupe "Business", clique sur le lien "Cancel" pour annuler et le groupe "Business" est toujours là. Des fois, l'informatique ça marche comme on voudrait...

Allez hop ! Encore un coup de tests unitaires pour me conditionner et c'est tout pour aujourd'hui.


Billet suivant dans la série : Test-Driven Development avec ASP.NET MVC (suite)

vendredi 9 octobre 2009

Test-Driven Development avec ASP.NET MVC (suite)

Je continue ma formation ASP.NET MVC avec la suite de la 6° partie du tutoriel pour gérer des contacts qui est consacrée à la Programmation Pilotée par les Tests (ou Test-Driven Development en anglais).

Point de départ

Jusqu'ici, j'ai donc abordé les deux premières étapes sur les trois que compte la méthode TDD :

  1. Ecrire un test unitaire qui échoue (Rouge)
  2. Ecrire un code qui passe le test unitaire avec succès (Vert)
  3. Revoir l’architecture de votre code (Refactoring)

Pour résumer l'épisode précédent, il fallait modifier l'application des gestion de contacts pour ajouter une notion de groupe de contacts. Comme point de départ, on avait 3 scénarios utilisateur (lister les groupes, créer un groupe et valider un groupe) et on a appliqué les 2 premières étape du TDD :

  • Etape 1 : on écrit des tests unitaires pour vérifier les fonctionnalités attendues (et ces tests échouent car non compilables)
  • Etape 2 : on écrit le minimum de code pour que les tests unitaires réussissent

Refactoring

J'en arrive donc à l'étape 3 : revoir l’architecture du code (refactoriser). Aux deux premières étapes du TDD, l'objectif est de se concentrer sur les tests unitaires destinés à vérifier que les fonctionnalités sont biens implémentées, sans se prendre la tête sur la "bonne" façon de faire. Ce n'est qu'à partir de cette troisième étape qu'il faut chercher à produire du "bon" code. Et on peut toucher à notre premier jet de code l'esprit libre, puisqu'on s'est blindé grâce à nos tests unitaires qui vont nous empêcher de casser quoi que ce soit d'important.

Dans le cas présent, le refactoring va consister à réviser le code du contrôleur GroupController qui mélange un peu tout pour le rendre plus conforme au Single Responsibility Principle comme on l'avait déjà fait dans la 4° partie du tutoriel pour le contrôleur ContactController.

Pour cela, on va modifier le contrôleur pour qu'il utilise la couche de service ContactManagerService que l'on avait mis en place pour le contrôleur ContactController. C'est assez simple et il suffit de taper le code source fourni dans le tutoriel. Il y manque quelques using ContactManager.Models.Validation; et il faut bien penser à modifier aussi les fichiers interfaces, mais c'est OK pour la 3° étape du TDD.

Ca avance, mais je me pose quand même des questions.

  • Est-ce que ça prendrait vraiment beaucoup plus de temps si on avait fait "bien" dès le premier coup ?
  • Et même, est-ce qu'une fois qu'on a l'habitude d'utiliser les couches service et repository, est-ce que ce n'est pas plus long de se souvenir comment écrire du code qui ne s'en sert pas ?
  • Et surtout, il n'y a pas de 4° étape dans le TDD et pourtant je n'ai encore rien qui gère des groupes de contacts pour de "vrai" !

Coup d'oeil en arrière

Sinon, cette partie du tutoriel m'a fait prendre conscience d'un truc auquel je n'avais pas fait attention lors de la partie sur le refactoring. En fait, la classe repository et la classe service servent pour toute l'application et pas seulement dans le cas de la table Contact. C'est d'ailleurs pour ça qu'elles s'appellent ContactManagerRepository et ContactManagerService et pas seulement ContactRepository et ContactService.

Ca non plus, je sais pas trop si ça me plait bien. J'ai un peu peur qu'à la fin on risque de se retrouver avec un source énorme si notre application doit gérer un très grand nombre de tables. Sans compter que ça devient génant dans le cas des FakeRepository où on devra donc tout implémenter à coup de throw new NotImplementedException() (et revenir dessus en permanence dès qu'on ajoutera des signatures à l'interface).

Mais je suppose qu'il n'est pas totalement incongru d'avoir des "sous" repository pour différents tables (ou peut-être des classes partielles ?). A voir...

Sous le coude

Je vais continuer avec la suite de cette 6° partie qui va enfin s'attaquer à la réalisation des fonctionnalités pour de "vrai". Mais si j'ai un peu de temps, j'aimerai bien essayer d'approfondir tout ça pour voir comment implémenter les autres scénarios utilisateurs selon la méthode TDD :

  • L’utilisateur peut supprimer un groupe de contacts existant
  • L’utilisateur peut sélectionner un groupe lorsqu’il crée un nouveau contact
  • L’utilisateur peut sélectionner un groupe lorsqu’il édite un contact existant
  • La liste des groupes de contacts est affichée dans la vue Index
  • Lorsqu’un utilisateur clique sur un groupe, la liste de contacts associée est affichée

(Bien que pour l'instant je ne vois pas trop quel test unitaire faire pour vérifier la suppression d'un groupe de contacts.)

Ou en attendant tout ça, deux liens assez intéressant sur les TDD :


Billet suivant dans la série : Test-Driven Development avec ASP.NET MVC (suite)

mardi 6 octobre 2009

Test-Driven Development avec ASP.NET MVC

Dans la sixième étape du tutoriel pour créer une application de gestion de contacts avec ASP.NET MVC, le but est de faire encore mieux que de simples tests unitaires et d'apprendre à développer selon les principes du "test-driven development" (= programmation pilotée par les tests) qui implique d'écrire d'abord les tests unitaires puis ensuite le "vrai" code fonctionnel en respectant le moule des tests unitaires. L'idée, c'est qu'à la fin du codage, on a forcément un code qui fait correctement ce qu'on avait prévu de faire (et accessoirement que ce qui était prévu de faire).

Par rapport aux parties précédentes, je vais détailler un peu plus ce que j'ai fait, parce que j'ai eu plus de mal à en venir à bout :

  • c'est assez compliqué, ou en tout cas assez nouveau pour moi
  • il ya sans doute quelques petites erreurs dans le code fourni en exemple
  • les modifications réalisées ne sont pas toujours toutes expliquées

Par contre, je ne vais pas revenir pas sur l'intérêt du TDD, la façon de le mettre en œuvre ou tous les avantages que cela apporte, mais plutôt sur la façon dont se déroule le tutoriel.

1° scénario : lister les groupes de contacts

Le premier contact avec le TDD a pour but de répondre au user story (= scénario utilisateur) suivant : "L’utilisateur peut voir une liste de groupes de contacts".

Je commence donc par taper le premier test unitaire destiné à tester que la méthode Index() du contrôleur Group renvoie bien un ensemble de groupes.

Mais comme pour l'instant, il n'y a pas encore de "vrai" code, l'IntelliSense ne m'aide pas vraiment voire me complique la vie en remplaçant GroupController() par GroupControllerTest() dès que je je tape la parenthèse ouvrante !

Mais bon, c'est le but... Le fait que rien ne marche (et que l'application ne compile plus) constitue la 1° étape du TDD, à savoir "écrire un test unitaire qui échoue". 1° étape TDD : OK (je trouve quand même que c'est un raisonnement un peu tiré par les cheveux).

Le bon côté des choses, c'est que cela m'a au moins permis d'identifier une petite différence entre MSTest :

Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable));

et NUnit :

Assert.IsInstanceOfType(typeof(IEnumerable), result.ViewData.Model);

Le truc important, c'est que l'ordre des paramètres est inversé. Et accessoirement, cette méthode IsInstanceOfType est maintenant obsolète avec NUnit et il faudrait employer :

Assert.IsInstanceOf(typeof(IEnumerable), result.ViewData.Model);

Quoiqu'il en soit, pour passer à l'étape suivante du TDD ("Ecrire un code qui passe le test unitaire avec succès"), il faut au minimum réussir à compiler. Pour cela, il faut modifier le projet pour ajouter la classe Controllers\GroupController.cs :

using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class GroupController : Controller
    {
        //
        // GET: /Group/

        public ActionResult Index()
        {
            var groups = new List();
            return View(groups);
        }

    }
}

Mais c'est malheureusement toujours impossible à compiler, puisqu'on obtient le message d'erreur Using the generic type 'System.Collections.Generic.List<T>' requires '1' type arguments. C'est sans doute pour cela que le tutoriel fait aussi saisir la classe Models\Group.cs (sinon elle semblait servir à rien) :

namespace ContactManager.Models
{
    public class Group
    {
    }
}

Il suffit alors de modifier la classe GroupController.cs pour y remplacer la ligne var groups = new List(); par var groups = new List<Group>(); pour que ça compile et que le test unitaire soit passé avec succès. 2° étape TDD : OK (déjà 5 pages de passées sur 21, ça avance vite!).

Pour relativiser un peu, on a "juste" mis au point la façon de vérifier que notre futur contrôleur GroupController aura bien une méthode Index() qui renverra une liste d'objets de type Group.

2° scénario : créer un groupe de contacts

Avant de passer à la programmation du "vrai" code, on remet ça avec la prise en compte d'un second scénario utilisateur, à savoir : "L’utilisateur peut créer un nouveau groupe de contacts".

Pour ça, il faudra donc que le contrôleur GroupController ait une méthode Create et que l'utilisation de cette méthode ajoute bien un nouveau groupe dans la liste des groupes. On va donc commencer par écrire un test unitaire qui contrôle ça.

[TestMethod]
public void Create()
{
    // Arrange
    var controller = new GroupController();
    // Act
    var groupToCreate = new Group();
    controller.Create(groupToCreate);
    // Assert
    var result = (ViewResult)controller.Index();
    var groups = (IEnumerable<Group>)result.ViewData.Model;
    CollectionAssert.Contains(groups.ToList(), groupToCreate);
}

Encore une fois, la 1° étape du TDD est OK par KO : le test échoue puisqu'on ne peut pas compiler. On peut donc se précipiter pour écrire le minimum du code nécessaire pour que le test réussisse. Pour ça, on ajoute une méthode action Create() à notre contrôleur GroupController et celle-ci ajoute l'objet Group qui lui est passé en paramètre à la collection des groupes. Pour que tout fonctionne, la collection des groupe est désormais renvoyée par l'action Index().

Notre contrôleur GroupController.cs contient donc le code suivant :

using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class GroupController : Controller
    {
        private IList<Group> _groups = new List<Group>();

        public ActionResult Index()
        {
            return View(_groups);
        }

        public ActionResult Create(Group groupToCreate)
        {
            _groups.Add(groupToCreate);
            return RedirectToAction("Index");
        }
    }
}

Zut! Ca ne compile toujours pas ! Il y a un problème au niveau de la ligne

CollectionAssert.Contains(groups.ToList(), groupToCreate);

dans la classe de test GroupControllerTest.cs :

'System.Collections.Generic.IEnumerable<ContactManager.Models.Group>' does not contain a definition for 'ToList' and no extension method 'ToList' accepting a first argument of type 'System.Collections.Generic.IEnumerable<ContactManager.Models.Group>' could be found (are you missing a using directive or an assembly reference?)

J'efface .ToList(), je colle un point après groups et l'IntelliSense me propose Equals, GetEnumerator, GetHashCode, GetType et ToString. C'est ma foi vrai qu'il n'y a pas de ToList() ! Que faire ? Je tente le coup en laissant juste :

CollectionAssert.Contains(groups, groupToCreate);

Ca compile et le test réussit. Que demander de plus ? Ca fait ma seconde 2° étape TDD OK de la journée. La suite sera pour plus tard...

Mise à jour du 2/12/9 : En fait, il manquait un using System.Linq; au début de GroupControlletTest.cs pour pouvoir utiliser .ToList().

3° scénario : valider la création d'un groupe de contacts

(Plus tard) Pour bien faire les choses, notre création d'un nouveau groupe de contacts est un peu limite. On ne peut pas en créer un comme ça à la bonne franquette, sans vérifier ce que saisi l'utilisateur. Ce qui implique un nouveau scénario utilisateur : "L’utilisateur ne peut pas créer un nouveau groupe de contacts sans lui donner un nom".

C'était pas une fonctionnalité prévue dans les scénarios utilisateurs du départ, mais cela semble logique. Soyons réactifs, on fait du TDD, pas du Cycle en V !

On écrit donc un test unitaire "CreateRequiredName" qui va s'assurer que l'on a bien une erreur quand on essaie créer un groupe de contacts sans lui donner de nom.

[TestMethod]
public void CreateRequiredName()
{
    // Arrange
    var controller = new GroupController();
    // Act
    var groupToCreate = new Group();
    groupToCreate.Name = String.Empty;
    var result = (ViewResult)controller.Create(groupToCreate);
    // Assert
    var error = result.ViewData.ModelState["Name"].Errors[0];
    Assert.AreEqual("Name is required.", error.ErrorMessage);
}

Attention, il faut également modifier le test unitaire "Create" précédent pour renseigner le nom du groupe afin qu'il continue à fonctionner :

...
// Act
var groupToCreate = new Group();
groupToCreate.Name = "Test";
controller.Create(groupToCreate);
...

Ca ne compile plus <=> le test unitaire échoue => on peut coder vite fait quelque chose pour le passer. (Finalement, c'est assez rigolo comme méthode).

Déjà, pour que cela ait une chance de marcher, il faut ajouter une propriété Name à notre classe Group. Pour cela, on a seulement besoin d'insérer une ligne pour définir une propriété automatique :

namespace ContactManager.Models
{
    public class Group
    {
        public string Name { get; set; }
    }
}

Ca re-compile, le test "Create" réussi toujours mais le test "CreateRequiredName" échoue => c'est pas fini : il faut encore vérifier que le nom du groupe n'est pas vide avant de le créer. Pour cela, il suffit d'ajouter quelques lignes à l'action "Create" du contrôleur GroupController :

public ActionResult Create(Group groupToCreate)
{
    // Validation logic
    if (groupToCreate.Name.Trim().Length == 0)
    {
        ModelState.AddModelError("Name", "Name is required.");
        return View("Create");
    }
    // Database logic
    _groups.Add(groupToCreate);
    return RedirectToAction("Index");
}

Mission accomplie : ça continue de compiler et tous les tests unitaires sont réussis.

Petit bilan avant de continuer

On progresse peu à peu sur la voie du développement piloté par les tests : création des scénarios utilisateur, écriture des tests unitaires et codage à la hussarde pour respecter ces tests. Mais il ne faut pas perdre de vue que ce n'est que le tout début du chemin :

  • la façon dont on a codé c'est du ni fait ni à faire...
  • on ne peut a toujours pas créer un groupe de contacts pour de "vrai" !

Billet suivant dans la série : Test-Driven Development avec ASP.NET MVC (suite)

mardi 29 septembre 2009

Unit Test Definition 2.0

A unit test is a fast, in-memory, consistent, automated and repeatable test of a functional unit-of-work in the system.

Ray Osherov, auteur de The Art of Unit Testing

lundi 28 septembre 2009

ASP.NET MVC et les tests unitaires

La cinquième étape du tutoriel pour développer une application de gestion de contacts avec ASP.NET MVC concerne les tests unitaires : à quoi cela sert, comment les utiliser et les créer... puis aborde la réalisation d'un premier jeu de tests unitaires pour contrôler le bon fonctionnement de la couche de service et un autre pour vérifier le fonctionnement du contrôleur.

Rendre NUnit compatible avec Visual Studio Unit Test

Pour commencer, comme je fais mes essais avec Visual Web Developer et pas Visual Studio 2008, je ne peux pas utiliser Visual Studio Unit Test, le framework de test par défaut. A sa place, j'utilise NUnit et pour l'instant je n'ai pas rencontré trop de problème (mais il est vrai que je n'ai pas encore fait grand chose...).

Ce coup-ci, cela devient un peu plus sérieux et il s'agit dans un premier temps de saisir une ou deux pages de tests unitaires pour tester le service ContactManagerService. J'ai carrément triché et fait du copié / collé pour d'avoir tout à saisir moi-même, mais le plus embêtant, c'est que le code proposé par le tutoriel ne fonctionne pas avec NUnit :(

Premier truc pas si compliqué à trouver : il faut supprimer la référence au framework Visual Studio Unit Test

using Microsoft.VisualStudio.TestTools.UnitTesting;

et la remplacer par une référence à NUnit :

using NUnit.Framework;

C'était malin, mais pas suffisant... Le couple VWD + NUnit n'aime pas du tout les attributs [TestClass], [TestInitialize] et [TestMethod] et me le fait savoir :

  • The type or namespace name 'TestClassAttribute' could not be found
  • The type or namespace name 'TestClass' could not be found
  • The type or namespace name 'TestInitializeAttribute' could not be found
  • The type or namespace name 'TestInitialize' could not be found
  • The type or namespace name 'TestMethodAttribute' could not be found
  • The type or namespace name 'TestMethod' could not be found

Après quelques recherches, j'ai finalement trouvé la solution chez static void (NUnit/MSTest Dual Testing). Il suffit d'ajouter quelques "usings" au début du source pour que NUnit accepte les mêmes attributs que VS Unit Test :

using TestClass = NUnit.Framework.TestFixtureAttribute;
using TestMethod = NUnit.Framework.TestAttribute;
using TestInitialize = NUnit.Framework.SetUpAttribute;
using TestCleanup = NUnit.Framework.TearDownAttribute;
using TestContext = System.Object;

Encore deux petites remarques :

  • VWD ne proposant pas l'option "Add / New Test / Unit Test", il faut choisir "Add / New Item / Class"
  • La méthode .Expect() de Moq a été remplacée par .Setup()

Sur quoi faire porter les tests unitaires

Sinon, l'étude de cette cinquième partie m'en a un peu plus appris sur l'intérêt des tests unitaires, notamment sur quoi les faire porter (la couche métier) ou plus exactement sur quoi "éviter" de les faire poster (la présentation et l'accès aux données). L'idée, c'est que les tests unitaires doivent s'exécuter très très très rapidement, sinon on risque de les laisser tomber, sauf si on est très joueur...

unit-testing.png

Il ne faut donc pas faire de tests unitaires sur l'interface utilisateur, parce que puisque on est en ASP.NET, cela exige le lancement d'un serveur Web ce qui ralentira la réalisation des tests. C'est une des raisons qui fait que MVC est "mieux" que les WebForms. Dans MVC, la View qui est la partie interface utilisateur n'est pas faite pour contenir du code (ce qui fait qu'on a pas à le tester !). Dans le cas des WebForms, où tout est plus ou moins mélangé, il est plus difficile d'isoler le code de la présentation.

Il ne faut pas non plus faire de tests sur l'accès aux bases de données. Si on doit en passer par une vraie base de données, le temps d'exécution des tests unitaires va devenir beaucoup trop long. Et c'est là que l'empilement des couches évoqué dans la partie précédente dévoile tout son intérêt. On va "émuler" l'accès à la base de données grâce à des classes Mock qui simulent l'implémentation de l'interface IRepository vue précédemment, sans réellement interagir avec la base de données. Par conséquent, la réalisation de cette interface IRepository ne doit pas être ressentie comme un niveau de complexité supplémentaire et inutile mais comme une étape clé pour faciliter la mise en place de tests unitaires.

Sous le coude

La sixième étape du tutoriel va consister à ajouter une fonctionnalité à la gestion des contacts. Cela promet d'être très intéressant car on va pour cela bouleverser l'application en introduisant la notion de groupe de contacts. Et ce coup-ci on va d'abord créer les tests unitaires à priori, dans le but d'encadrer la programmation des modifications.

Et à lire pour plus tard, un article d'une quinzaine de pages en français sur les tests unitaires en pratique par Patrick Smacchia. Dans un premier temps, il présente comment réaliser des tests unitaires et les bénéfices que l'on peut en retirer. La seconde partie est consacrés aux problèmes de mise en oeuvre que l'on peut rencontrer (il aborde un peu plus en détail les problématiques liées à l'accès à la base de données) et comment les résoudre.


Billet suivant dans la série : Test-Driven Development avec ASP.NET MVC