MPOWARE, société d'édition de logiciels et de prestation de services en technologies & architectures .NET & Visual Studio 2008 - http://www.mpoware.com
Publié mardi 18 mars 2008 18:16 par MPOWARE

Implémentation d’un contrôleur MVC testable

Faisant suite à un précédent article sur l’implémentation d’un contrôleur MVC pour l’interopérabilité MFC<->Windows Forms<->WPF et qui en mentionnait la testabilité, cet article se propose de détailler l’implémentation de ce contrôleur ainsi que la manière de tester la logique d’une couche IHM hors IHM elle-même (nous reviendrons sur ce point à la fin de l’article).

La philosophie du pattern MVC est d’introduire un découplage entre le rendu graphique de l’application, son fonctionnement et les données qu’elle affiche. Ces trois rôles sont symbolisés par l’acronyme MVC qui s’exprime de la manière suivante :

·         M pour Model : les données de l’application,

·         V pour View : c’est le rendu graphique des données de l’application,

·         C pour Controller : c’est la logique de l’IHM.

Sans détailler plus avant le pattern MVC (il existe nombre d’articles sur le sujet sur le Web), il est intéressant de noter que les formes d’implémentation du pattern sont diverses et variées et que le pattern MVC peut se retrouver sous d’autres formes variantes (tel que le pattern MVP par exemple).

Dans cet article, nous proposons une implémentation concrète d’un pattern MVC dont l’objectif est de permettre d’appliquer des tests (unitaires ou d’intégration) sur la partie contrôleur. L’implémentation recherche à permettre l’automatisation de tests sur la logique de présentation. Cette démarche est très similaire à celle qui consiste à tester unitairement des instances de classes métier / données dans Visual Studio 2008. Cet IDE ne propose pas d’outil de génération et d’exécution automatique de tests d’IHM Windows bien qu’il existe un outil similaire pour les applications Web (encore un fois, nous reviendrons sur ce point en fin d’article pour expliquer les solutions actuelles qui peuvent être envisagées dans ce cas). Ainsi, sans outil de tests, la conception MVC peut nous permettre d’isoler la logique de l’interface utilisateur afin de bénéficier des outils de tests standards de Visual Studio.

L’application exemple

Pour illustrer l’implémentation du contrôleur MVC testable, nous utiliserons une solution composée de 3 projets pour bien démontrer la séparation physique des couches applicatives. Dans la réalité, nous retrouvons en général la vue et le contrôleur dans le même projet (mais isolés par namespace) et le model dans un projet à part.

L’application exemple est donc constituée de :

·         Une application .NET Windows Forms,

·         Une librairie de classes de type « contrôleur »,

·         Une librairie de classes pour les entités et composants dits « métier ».

La solution sera composée d’un projet supplémentaire pour les tests unitaires.

L’application en elle-même n’a d’autre objectif que celui de montrer une implémentation concrète de tests d’un contrôleur d’IHM. Le fonctionnel de cette application n’a pas d’objectif ni de sens précis pour cette implémentation.

Le « Model »

Les données « métier » permettront d’accéder à une liste de personnes et aux détails (nom, prénom) d’une personne. Nous aurons 2 classes pour nos besoins : Person et PersonList.

public class Person : INotifyPropertyChanged

{

    private string _firstname;

    private string _lastname;

 

    public Person(string firstname, string lastname)

    {

        _firstname = firstname;

        _lastname = lastname;

    }

 

    public string Firstname

    {

        get { return _firstname; }

        set { _firstname = value; RaisePropertyChanged("Firstname"); }

    }

 

    public string Lastname

    {

        get { return _lastname; }

        set { _lastname = value; RaisePropertyChanged("Lastname"); }

    }

 

    private void RaisePropertyChanged(string propertyName)

    {

        if (PropertyChanged != null)

        {

            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

        }

    }

 

    #region INotifyPropertyChanged Members

 

    public event PropertyChangedEventHandler PropertyChanged;

 

    #endregion

}

public class PersonList : List<Person>

{

    public PersonList()

    {

        this.Add(new Person("Frédéric", "Queudret"));

        this.Add(new Person("Guillaume", "Queudret"));

    }      

}

A noter l’implémentation de l’interface INotifyPropertyChanged par la classe Person qui permet de déclencher l’événement PropertyChanged sur la mise à jour d’une propriété.

 

Le « Controller »

Le contrôleur permet de découpler l’IHM de la donnée applicative en implémentant la gestion des événements et actions que l’utilisateur peut déclencher / exécuter. C’est le contrôleur qui implémente la logique « UI » (de l’Interface Utilisateur) et c’est cette logique que nous pourrons ensuite référencer pour automatiser nos tests.

Déclaration du contrôleur SampleController :

public abstract class Controller

{

}

 

public class SampleController : Controller

{

 

}

La logique de l’application voulant que le contrôleur permette l’interaction avec le « model », nous référençons le projet de librairie de classes « métier » (nommé MVCSampleModel pour l’exemple) depuis le projet contrôleur (MVCSampleController). La classe de base Controller nous permet d’anticiper le besoin de polymorphisme de classe que nous aurons plus tard lorsque notre application sera composée de plusieurs contrôleurs.

Nous pouvons ensuite écrire le code suivant :

public class SampleController : Controller

{

    private PersonList _persons;

    private BindingSource _dataSource;      

 

    public SampleController()

    {

        _persons = new PersonList();

        _dataSource = new BindingSource();

        foreach (Person person in _persons)

        {

            _dataSource.Add(person);

        }           

    }

 

    public BindingSource DataSource

    {

        get { return _dataSource; }

        set { _dataSource = value; }

    }

 

    public PersonList Persons

    {

        get { return _persons; }

        set { _persons = value; }

    }      

}

La propriété DataSource permet de lier les données manipulées par le contrôleur à tout objet supportant le DataBinding (exemple : DataGridView).

La « View »

Dans cet exemple, la vue est implémentée sous la forme d’une « Form » Windows Forms. Cette vue pourrait être implémentée sous la forme d’un User Control, le principe serait identique. Nous référençons le projet MVCSampleController (projet contenant les contrôleurs) et le projet MVCSampleModel (notre couche métier).

L’implémentation de la classe partielle de notre « Form » est la suivante :

public partial class MainForm : Form

{

    private SampleController _controller;

 

    public MainForm()

    {

        InitializeComponent();

        _controller = new SampleController();

        personsGridView.AutoGenerateColumns = true;

        personsGridView.DataSource = _controller.DataSource;

    }

}

A l’exécution, la fenêtre principale affiche la liste des personnes dans un DataGridView (personsGridView).

La logique du pattern MVC est en place, il ne reste plus qu’à ajouter des fonctions à notre application et donc des éléments graphiques permettant à l’utilisateur d’exécuter ces fonctions.

Quelques fonctions…

Ajoutons deux fonctions permettant d’afficher la liste des personnes avec noms et prénoms en majuscule et en minuscule. Nous ajoutons 2 boutons à notre fenêtre principale pour permettre à l’utilisateur d’utiliser la mise en majuscule / minuscule.

Nous ajoutons ensuite le code nécessaire à ces fonctions dans la classe contrôleur (SampleController), de la manière suivante :

    public void ToUpper()

    {

        foreach (Person person in _persons)

        {

            person.Firstname = person.Firstname.ToUpper();

            person.Lastname = person.Lastname.ToUpper();

        }  

    }

 

    public void ToLower()

    {

        foreach (Person person in _persons)

        {

            person.Firstname = person.Firstname.ToLower();

            person.Lastname = person.Lastname.ToLower();

        } 

    }

Puis nous implémentons le code correspondant sur le déclenchement de l’événement Click des 2 boutons de l’IHM :

    private void toUpperButton_Click(object sender, EventArgs e)

    {

        _controller.ToUpper();

    }

 

    private void toLowerButton_Click(object sender, EventArgs e)

    {

        _controller.ToLower();

    }

Vous pouvez alors exécuter l’application pour tester le code de mise en majuscule / minuscule.

Nous ajoutons ensuite à l’IHM 2 labels, 2 textbox et un bouton permettant d’ajouter une nouvelle personne à la liste des personnes. L’événement Click déclenché sur le bouton d’ajout d’une personne à la liste va solliciter le contrôleur pour exécuter cette action. Le code du contrôleur pour l’ajout d’une personne à la liste est le suivant:

    public void Add(string firstname, string lastname)

    {

        Person newPerson = new Person(firstname, lastname);

        _persons.Add(newPerson);

        _dataSource.Add(newPerson);

    }

Le code du bouton est un simple appel à la méthode exposée par le SampleController en passant les « inputs » de l’utilisateur : _controller.Add(firstnameTextBox.Text, lastnameTextBox.Text);

Les fonctions implémentées ont montré comment exécuter des « actions » du contrôleur ou encore exécuter une logique implémentée dans le contrôleur lors du déclenchement d’un événement sur l’interface utilisateur. Nous allons implémenter une dernière fonction permettant de mettre à jour en retour l’IHM suite au déclenchement d’une action.

Pour cela, nous ajoutons un label à l’IHM permettant d’afficher un message utilisant la concaténation des propriétés de la personne sélectionnée dans la DataGridView.

Le code du contrôleur est le suivant :

    public event EventHandler<SelectedPersonChangedEventArgs> SelectedPersonChanged;

 

    public void RefreshCurrentSelection()

    {

        Person currentPerson = _dataSource.Current as Person;

        if (currentPerson != null)

        {

            RaiseSelectedPersonChanged(string.Format("{0} {1}", currentPerson.Firstname, currentPerson.Lastname));

        }

    }

 

    public void RaiseSelectedPersonChanged(string fullname)

    {

        if (SelectedPersonChanged != null)

        {

            SelectedPersonChanged(this, new SelectedPersonChangedEventArgs(fullname));

        }

    }

Ce code implémente une action de demande de rafraichissement sur la sélection courante. Ce rafraichissement s’opère s’il existe une sélection en cours et déclenche un événement de rafraichissement. L’argument véhiculé lors du déclenchement est la concaténation du prénom et nom de la personne sélectionnée. La classe permettant de véhiculer les arguments de l’événement est SelectedPersonChangedEventArgs :

public class SelectedPersonChangedEventArgs : EventArgs

{

    private string _fullname;

    public SelectedPersonChangedEventArgs(string fullname)

    {

        _fullname = fullname;

    }

 

    public string Fullname

    {

        get { return _fullname; }

    }

}

Le code de la vue évolue lui aussi :

    private void personsGridView_Click(object sender, EventArgs e)

    {

        _controller.RefreshCurrentSelection();

    }

   

void Controller_SelectedPersonChanged(object sender, SelectedPersonChangedEventArgs e)

    {

        currentPerson.Text = e.Fullname;

    }

 

Il faut ensuite ajouter la référence au gestionnaire d’événement Controller_SelectedPersonChanged :

_controller.SelectedPersonChanged += new EventHandler<SelectedPersonChangedEventArgs>(Controller_SelectedPersonChanged);

Ce qui nous permet d’implémenter une majorité de scenarios d’utilisation du contrôleur. Nous obtenons finalement le rendu suivant :

 

Tests de la logique de l’IHM

L’application en cours de développement doit pouvoir être testée. D’ordinaire, les tests portent sur les classes qui n’opèrent pas de rendu ou n’attendent pas d’actions utilisateurs non simulables pour être testées. Dans notre exemple, le contrôleur est rendu « testable » par le fait qu’il n’a pas de dépendance ou de couplage fort à la vue qui l’utilise. Cela s’illustre d’ailleurs par les dépendances de projet qui montrent bien que l’IHM dépend du contrôleur et non l’inverse.

Nous ajoutons un projet de tests à la solution et générons le code des tests unitaires, puis modifions le code des tests unitaires de la sorte :

    /// <summary>

    ///A test for Add

    ///</summary>

    [TestMethod()]

    public void AddTest()

    {

        SampleController target = new SampleController();

        string firstname = "sample firstname";

        string lastname = "sample lastname";

        int countBeforeAdd = target.Persons.Count;

        target.Add(firstname, lastname);

        int countAfterAdd = target.Persons.Count;

        Assert.IsTrue((countBeforeAdd + 1) == countAfterAdd);

    }

 

    /// <summary>

    ///A test for RefreshCurrentSelection

    ///</summary>

    [TestMethod()]

    public void RefreshCurrentSelectionTest()

    {

        bool isRaised = false;

        ManualResetEvent raisedEvent = new ManualResetEvent(false);

 

        SampleController target = new SampleController();

        target.SelectedPersonChanged += delegate {isRaised = true; raisedEvent.Set(); };

 

        target.RefreshCurrentSelection();

 

        raisedEvent.WaitOne(2000, false);

        Assert.IsTrue(isRaised);

    }

Nous n’avons montré ici que les tests intéressants et notamment le dernier RefreshCurrentSelectionTest, qui montre un exemple simple de test de l’output du contrôleur.

L’exécution des tests dans Visual Studio se traduit par la sortie suivante :

 

Où sommes-nous et prochaines étapes

L’exemple d’implémentation du pattern MVC donné dans cet article permet aux développeurs de découpler le rendu de sa logique et propose une implémentation de la logique d’interface sous la forme d’un contrôleur « testable ». Les tests sur le contrôleur peuvent alors être automatisés, de sorte que ces tests permettront de vérifier la non-régression lors de l’évolution de l’application.

La démarche proposée est simple et facile à mettre en œuvre. Malgré tout, elle a le désavantage de demander aux développeurs l’implémentation d’une quantité de code importante notamment pour le retour de données depuis le contrôleur (déclaration d’un événement, déclenchement de l’événement, déclaration le cas échéant d’une classe portant les arguments de l’événement…). Mais les bénéfices de cette conception sont importants et non négligeables : faible dépendances entre le rendu et la logique d’interface, possibilité de tester la logique d’interface et surtout permettre l’évolutivité de la technologie de l’interface sans impacter son comportement. Ainsi, la migration d’une application, dont l’IHM a été développée en suivant ces principes, pourrait très facilement basculer vers une IHM WPF ou autre technologie de rendu si besoin était.

Il ne faudra pas oublier non plus les outils qui permettent de tester automatiquement des IHM en simulant les actions des utilisateurs. Pour ceux qui n’auraient pas eu l’envie ou la possibilité d’implémenter les principes de conception vus dans cet article, il reste encore l’ultime solution d’utiliser un outil externe permettant d’enregistrer les actions utilisateurs et les rejouer automatiquement ultérieurement. Vous pourrez trouver plus d’informations sur cet outil à l’adresse suivante : http://www.mpoware.com/fr-fr/Products/AutomatedUITestsTool.aspx.

Cas des contrôleurs asynchrones

Un contrôleur asynchrone est un contrôleur qui lance l’exécution d’une action (généralement longue) et qui rend la main au thread de l’interface afin que l’IHM reste utilisable par l’utilisateur pendant l’exécution de la tâche longue. L’exécution asynchrone permet par exemple de réaliser un long calcul (plusieurs secondes) tout en laissant l’utilisateur exécuter d’autres actions.

Le code suivant propose une implémentation basée sur notre contrôleur mais faisant appel à une méthode dont l’exécution est longue et dont les résultats intermédiaires seront visibles dans l’IHM sous la forme d’une barre de progression. Pendant l’exécution de cette méthode, l’utilisateur peut continuer d’utiliser l’application sans que cette dernière ne soit « gelée ».

    public event EventHandler<DoWorkUpdateEventArgs> DoWorkUpdateEvent;

 

    public void DoWork(TimeSpan amountOfTime, TimeSpan showProgressInterval)

    {

        Thread workerThread = new Thread(new ParameterizedThreadStart(LongRunningMethod));

        workerThread.Start(new object[2] {amountOfTime, showProgressInterval});

    }

 

    protected void LongRunningMethod(object parameter)

    {

        object[] parameters = (object[])parameter;

        TimeSpan amountOfTime = (TimeSpan)parameters[0];

        TimeSpan showProgressInterval = (TimeSpan)parameters[1];

 

        DateTime endOfWork = DateTime.Now.Add(amountOfTime);

        double totalTicks = amountOfTime.TotalMilliseconds;

 

        while (DateTime.Now < endOfWork)

        {

            Thread.Sleep(showProgressInterval);

            double remainingTicks = endOfWork.Subtract(DateTime.Now).TotalMilliseconds;

            double percentWorkDone = 100.0 - (100.0 * (remainingTicks / totalTicks));

            RaiseDoWorkUpdateEvent(percentWorkDone);

        }

    }

 

    public void RaiseDoWorkUpdateEvent(double percentWorkDone)

    {

        if (DoWorkUpdateEvent != null)

        {

            DoWorkUpdateEvent(this, new DoWorkUpdateEventArgs(percentWorkDone));

        }

    }

Note : le lecteur ne tiendra pas compte du style d’implémentation de cet exemple notamment sur la partie du passage des paramètres au « worker » thread. Ceci n’est pas une bonne pratique mais le code est beaucoup plus synthétique, ce qui est l’objectif recherché ici.

La classe permettant de passer les paramètres à la vue :

public class DoWorkUpdateEventArgs : EventArgs

{

    private double _percentWorkDone;

    public DoWorkUpdateEventArgs(double percentWorkDone)

    {

        _percentWorkDone = percentWorkDone;

    }

    public double WorkDone

    {

        get { return _percentWorkDone; }

    }

}

Et enfin le code de la vue permettant de récupérer le temps d’exécution, l’intervalle de mise à jour de la barre de progression, ainsi que le rafraichissement de la barre de progression :

    private void doLongWorkButton_Click(object sender, EventArgs e)

    {

        TimeSpan timeToWork = new TimeSpan(0,0, int.Parse(workTimeTextBox.Text));

        // Afficher la progression toutes les secondes

        _controller.DoWork(timeToWork, new TimeSpan(0,0,1));

    }

 

    protected delegate void UpdateWorkProgress(object sender, DoWorkUpdateEventArgs e);

 

    void Controller_DoWorkUpdateEvent(object sender, DoWorkUpdateEventArgs e)

    {

        if (this.InvokeRequired)

        {

            // Le code est exécuté par un autre thread que le thread UI

            UpdateWorkProgress updateProgress = new UpdateWorkProgress(Controller_DoWorkUpdateEvent);

            this.Invoke(updateProgress, new object[] { sender, e });

        }

        else

        {

            // Le code est exécuté par le thread UI

            workProgressBar.Value = (int)e.WorkDone;

        }

    }

 

Ci-dessous la copie d’écran de l’application implémentant le contrôleur asynchrone. L’ajout des personnes se fait pendant l’exécution de la tâche longue dont la barre de progression montre l’évolution de l’exécution. L’interface n’est pas « gelée ».

 

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

# re: Implémentation d’un contrôleur MVC testable @ mercredi 19 mars 2008 13:27

excellent article ! On rappelera que l'implémentation MVC sera disponible pour les applications ASP.net avec les extensions 3.5

minsou


Les 10 derniers blogs postés

- Silverlight 3 : Communication et multicast par Kévin Gosse le il y a 6 heures et 28 minutes

- [Perso] Découvertes estivales : Linux (Part I) par Le blog de FremyCompany le il y a 9 heures et 10 minutes

- [Refactoring] ReSharper pour Visual Studio 2010 (Preview) par Thomas Jaskula le il y a 23 heures et 46 minutes

- [Refactoring] Analyser vos exceptions avec ReSharper Exceptional par Thomas Jaskula le 07-03-2009, 23:36

- SharePoint 2007 : patterns & practices SharePoint Guidance par Philippe Sentenac [MVP SharePoint] le 07-03-2009, 09:56

- [Visual Studio 2010] Les tests cases c’est bien, mais je vais devoir tout réécrire ? par Etienne Margraff le 07-03-2009, 09:00

- MVP[Gribouillon].AddYear par The Grib's Lair [Sébastien PICAMELOT - MVP SharePoint] le 07-03-2009, 08:45

- Clinique INSIA - Projet de fin d’Etudes (Silverlight 3 MVVM et OutOfBrowser, WCF, TFS) - Part 1 par David REI le 07-02-2009, 23:38

- C’est la crise ? Bah pourquoi cramer du budget pub alors ? par Nix's Blog le 07-02-2009, 15:31

- Soyons MVP ! par TheSaib .NET blog le 07-02-2009, 12:15