Utilisation de Frameworks de Mocking en .Net

Dans cet article, on va voir comment on peut, en utilisant un Framework dédié, fournir un moyen de tester l'interaction entre les objets de notre code, ou même entre nos objets et des entités extérieures au code, telles que des bases de données ou le système.

N'hésitez pas à commenter cet article ! Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

I-A. Le pattern de test "objet simulacre" (Mock Object)

L'objectif des tests unitaires est de tester une méthode à la fois, en isolation du reste du système.
Un problème de façon récurrente, à savoir celui de tester du code qui dépend d'autres objets.

Comment faire pour tester une méthode qui utilise une base de données ?
Comment tester la validité d'une écriture dans un log ?
De façon générale, comment peut-on tester du code faisant intervenir un objet pour lequel l'initialisation est plus importante que le test que l'on pensait effectuer ?

C'est exactement pour résoudre ce genre de problème que l'on va utiliser le pattern de test "objet simulacre".
Un "objet simulacre" est un objet qui va remplacer un des objets réels de notre solution, mais qui va retourner un résultat que l'on va prédéfinir. Non seulement ce pattern va nous permettre de tester notre logique de code en isolation, mais il va aussi permettre de valider les interactions entre objets. En effet, les Frameworks de simulacres permettent de définir des attentes (expectations), qui devront être satisfaites pour que le test réussisse.

L'utilisation des simulacres à comme avantage quasi immédiat d'augmenter considérablement la couverture de tests. En effet, ils vont nous permettre de tester des niveaux et des objets normalement fastidieux à tester, en quelques lignes de code.

I-B. Quand utiliser des Simulacres ?

On peut utiliser des simulacres dans les cas suivants :

  • L'objet réel a un comportement non déterministe (il produit des résultats imprévisibles, comme une date ou la température actuelle)
  • L'objet réel est difficile à mettre en place, ou est lent (cas d'une base de données, ou d'un service web)
  • Le comportement de l'objet réel est difficile à déclencher (par exemple, un problème de réseau)
  • L'objet réel a (ou est) une interface utilisateur
  • Le test doit demander à l'objet la manière dont elle est utilisée (par exemple, un test peut avoir besoin de confirmer qu'une fonction a été effectivement appelée)
  • L'objet réel n'existe pas encore (un problème courant lorsque l'on doit interagir avec d'autres équipes ou de nouveaux systèmes matériels)

Dans cet article, nous allons voir comment utiliser le pattern, d'abord en utilisant un objet créé à cet effet, puis à l'aide d'un Framework de simulacres.

II. L'application

Je vais utiliser, comme fil rouge dans ce tutoriel, une petite application de gestion, basée sur la base de données Northwind Access. (Disponible ici).

Mon application va avoir l'architecture suivante :

  • une application winform, utilisée comme couche de présentation
  • une couche métier
  • une couche d'accès aux données
  • une couche Core, contenant les fonctionnalités de log.
Image non disponible

Pour l'exemple, je vais développer une fenêtre permettant de visualiser les différents fournisseurs, et de les éditer.

Je vais, pour cela, utiliser uniquement des objets de base du Framework. Dans un premier temps, je vais construire ma solution. Attention: bien que les simulacres puissent être utilisés en TDD, dans mon exemple, on va tester après le développement.

Je vais produire deux écrans, un qui affiche la liste des fournisseurs, avec un bouton qui va permettre de sélectionner un fournisseur, pour l'éditer, un bouton de création, un bouton de suppression, et un bouton pour quitter l'application.

Image non disponible

Le second écran, lui, va comporter un champ pour chacun des champs de la base de données que je veux éditer, un bouton annuler, et un bouton ok

Image non disponible

Le projet, dans l'état initial (avec le code pour les insertions, mises à jour et sélection), est disponible ici.

Notre premier problème va être le suivant :
Comment tester que le traitement de mes données fonctionne, alors qu'on ne maîtrise pas les performances de la base de données ?

En effet, une des principales caractéristique de nos tests unitaires est qu'ils doivent être rapides et indépendant les uns des autres.
De plus, ils doivent avoir lieu dans un environnement maîtrisé.

Pour pouvoir contrôler l'environnement, nos choix vont donc être :

  • soit de créer notre base de données a l'initialisation des tests, et de la détruire en fin de test, ce qui est coûteux en terme de temps
  • soit de fournir "manuellement" les données à notre couche métier, en remplaçant l'objet fournisseur de données

Attention, la première approche est une approche tests d'intégration sur base de données.
Je reviendrais probablement sur les test d'intégration dans un prochain article, mais dans l'immédiat, on va utiliser nos simulacres pour exploiter la première approche.

III. Une première approche : un simulacre "manuel"

La première étape du tutoriel va être de créer notre simulacre de façon manuelle, en transpirant, "a l'ancienne".

Pour cela, on va refactoriser le code, en ajoutant une interface pour notre couche d'accès aux données. De plus, je vais ajouter une propriété permettant de changer l'objet DataAccess utilisé par ma SuppliersFactory.

Pour l'exemple, je vais créer mon interface au niveau de SuppliersDataAccess, qui va hériter d'une interface ISupplierDataAccess. Dans la vraie vie, j'adopterais plus probablement une approche différente, avec une IDataBaseFactory, qui encapsulerait mes classes d'accès a la base de données au niveau en dessous… et j'utiliserais un Framework d'injection de dépendance pour modifier le type de IDatabaseFactory utilisé à l'exécution.

III-A. Modifications dans la couche d'accès aux données

On va donc, dans un premier temps, ajouter une nouvelle interface ISuppliersDataAccess

 
Sélectionnez
using System;
using System.Data;
using System.Data.OleDb;
using System.Data.Common;
using Core;

namespace DataAccess {
        public interface ISuppliersDataAccess {
        
            DataSet GetAllSuppliers();
            DataRow GetOneSuppliers(int Supplierid);
            bool InsertSuppliers(string Companyname, string Contactname, string Address, string City, string Country);
            bool UpdateSuppliers(int Supplierid, string Companyname, string Contactname, string Address, string City, string Country);
            bool Exists(int Supplierid);
        }
}

Et ensuite, on va modifier le fichier SuppliersDataAccess pour faire hériter la classe de l'interface nouvellement créée .

 
Sélectionnez
namespace DataAccess {
    public class SuppliersDataAccess : ISuppliersDataAccess {
……….
   }
}

III-B. Modifications dans la couche métier

Au niveau de la couche métier, on va aussi modifier la classe SuppliersFactory qui, actuellement, utilise directement SuppliersDataAccess.

Comme je vais faire mes modifications de dépendances a la main, et pour ne pas modifier le code de mon client, je vais ajouter une petite propriété, qui va me retourner le ISupplierDataAccess que je veux utiliser.

 
Sélectionnez
private static ISuppliersDataAccess _dataFactory;

public static ISuppliersDataAccess DataFactory {
    get {
        if (_dataFactory == null) _dataFactory = new SuppliersDataAccess();
        return _dataFactory;
    }
    set { _dataFactory = value; }
}

Ensuite, je vais remplacer mes appels à SuppliersDataAccess par des appels à DataFactory.

Par exemple :

 
Sélectionnez
public static bool Update(int Supplierid, string Companyname, string Contactname, string Address, string City, string Country) {
        SuppliersDataAccess uda = new SuppliersDataAccess();
        return uda.UpdateSuppliers(Supplierid, Companyname, Contactname,  Address, City, Country);
}

Va devenir :

 
Sélectionnez
public static bool Update(int Supplierid, string Companyname, string Contactname, string Address, string City, string Country) {
        return DataFactory.UpdateSuppliers(Supplierid, Companyname, Contactname, Address, City, Country);
}

III-C. Création des tests

Je vais d'abord créer mon projet de tests, lui ajouter un répertoire lib, dans lequel je vais ajouter ma dll Nunit, et ajouter des références a mes projets Business et Data.

Enfin, je vais créer mon premier test.

Je ne vais pas tester directement SuppliersDataAccess, mais une nouvelle classe, mon premier simulacre, MockSuppliersDataAccess

MockSuppliersDataAccess doit implémenter ISupplierDataAccess pour que je puisse l'utiliser. Mon test va être un test tres basique, a savoir que je vais juste appeler la fonction GetAllSuppliers, et coder en dur un dataset que je vais renvoyer a ma factory.
Dans les faits, mon simulacre est donc plus un "bouchon" (stub) qu'un réel simulacre.

 
Sélectionnez
namespace DataAccess {
    public class MockSuppliersDataAccess : ISuppliersDataAccess {

        public DataSet GetAllSuppliers() {

            DataTable dt = new DataTable();
            dt.Columns.Add("SupplierID", typeof(int));
            dt.Columns.Add("CompanyName", typeof(string));
            dt.Columns.Add("ContactName", typeof(string));
            dt.Columns.Add("City", typeof(string));
            dt.Columns.Add("Country", typeof(string));

            DataRow dr;

            dr = dt.NewRow();
            dr["SupplierID"] = 1;
            dr["CompanyName"] = "Fournisseur 1";
            dr["ContactName"] = "Contact 1";
            dr["City"] = "Ville 1";
            dr["Country"] = "Pays 1";

            dt.Rows.Add(dr);

            dr = dt.NewRow();
            dr["SupplierID"] = 2;
            dr["CompanyName"] = "Fournisseur 2";
            dr["ContactName"] = "Contact 2";
            dr["City"] = "Ville 2";
            dr["Country"] = "Pays 2";

            dt.Rows.Add(dr);

            DataSet ds = new DataSet();
            ds.Tables.Add(dt);

            return ds;
        }
    }
}

Du coté des tests unitaires, je vais faire un appel assez standard a ma factory, mais je vais, juste avant cet appel, injecter mon MockSuppliersDataAccess a la place du DataAccess par défaut.

 
Sélectionnez
namespace Test {
    /// <summary>
    /// Some generated Tests.
    /// </summary>
    [TestFixture]
    public class TestSuppliersFactory {

        [SetUp]
        public void Init() {
        }

        [Test]
        public void TestGetAllSupplier() {
            SuppliersFactory.DataFactory = new MockSuppliersDataAccess();

            List<Suppliers> liste = SuppliersFactory.GetAllSuppliers();

            Assert.AreEqual(liste.Count, 2);
            Assert.AreEqual(liste[0].Supplierid, 1);
            Assert.AreEqual(liste[1].Supplierid, 2);
        }
    }
}

Le projet, dans l'état actuel, est disponible ici.

Le problème majeur de cette approche est qu'une fonction ne pourra être testée qu'avec un seul type de retour. On ne pourra pas tester le cas ou ma fonction renvoie null, à moins de faire n classes, avec des retours de données différentes.

On va donc utiliser un framework mieux adapte a ce problème.

IV. IV. Utilisation du Framework Rhino Mock

IV-A. Qu'est-ce que Rhino Mock ?

Rhino Mock est un Framework de simulacres, développé par Oren Eini (http://www.ayende.com/).

Pour reprendre la description de la page d'accueil du site officiel, Rhino Mock est :
Un Framework de simulacres dynamiques pour la plateforme .Net. Son but est de faciliter les tests, en permettant au développeur de créer des implémentations d'objets spécifiques et de vérifier les interactions en utilisant des tests unitaires.

Rhino Mock est un Framework mature, en version 3.4 actuellement, bien documenté et utilisé par une partie non négligeable de la communauté .Net.

Oren Eini est un contributeur de projets open source tels que Castle, Nhibernate et Rhino, et son blog est une mine de bon conseils en terme de tests et de design (son statut de robot est activement discute sur certaines listes, surtout depuis qu'il a dépassé les 3000 posts... en 4 ans le 1er avril 2008).

IV-B. Remplacement du test existant, et ajout des nouveaux tests

Dans un premier temps, je vais, sans pitié, effacer la classe MockSuppliersDataAccess, et mon test. Le fait d'utiliser un Framework de simulacres dynamique va me permettre de supprimer ce code de mes classes d'accès aux donnés.

La première de mes actions va être de revoir mon premier test, à l'intérêt assez limité... Mon test d'origine vérifiait que si je passais un Dataset à ma classe Factory, il me générait bien mes objets Suppliers. Le nouveau va vérifier que le comportement de la factory est adéquat si le DataAccess renvoie null ou pas de données.

Rhino nécessite impérativement de travailler sur des interfaces.
Dans notre exemple actuel, c'est déjà le cas, ce qui m'évite une refactorisation supplémentaire, mais si vous voulez pouvoir utiliser ce Framework, n'oubliez surtout pas cette règle de base !!!

Mon nouveau test est le suivant :

 
Sélectionnez
[Test]
public void Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data() {

    MockRepository mocks = new MockRepository();
    ISuppliersDataAccess da = mocks.CreateMock<ISuppliersDataAccess>();

    using (mocks.Record()) {
        Expect
            .Call(da.GetAllSuppliers())
            .Return(new DataSet());
        Expect
            .Call(da.GetAllSuppliers())
            .Return(null);
    }

    SuppliersFactory.DataFactory = da;

    using (mocks.Playback()) {
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
    }
}

On va reprendre ce code pièce par pièce...

 
Sélectionnez
MockRepository mocks = new MockRepository();
ISuppliersDataAccess da = mocks.CreateMock<ISuppliersDataAccess>();

On initialise le conteneur de simulacres. Cette action est nécessaire pour pouvoir utiliser nos simulacres. Ensuite, on déclare un simulacre de ISuppliersDataAccess. Ce simulacre va avoir les mêmes caractéristiques qu'une implémentation standard d'un ISuppliersDataAccess.

 
Sélectionnez
 using (mocks.Record()) {
        Expect
            .Call(da.GetAllSuppliers())
            .Return(new DataSet());
        Expect
            .Call(da.GetAllSuppliers())
            .Return(null);
    }

On initialise ensuite les attentes du conteneur.
Ces lignes informent le conteneur qu'il doit s'attendre à ce que da.GetAllSuppliers soit appelé deux fois de façon consécutive, et qu'il va retourner, la première fois, un nouveau dataset, et, la seconde fois, null

 
Sélectionnez
    using (mocks.Playback()) {
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
    }

On demande enfin au conteneur d'exécuter les actions SuppliersFactory.GetAllSuppliers en mode playback.
Ce mode va permettre de vérifier les attentes du conteneur. Si j'omets une des lignes, on aura une erreur à l'exécution du test, car le conteneur s'attend à deux appels à SuppliersFactory.GetAllSuppliers.

Mon premier test, réécrit, aura maintenant l'aspect suivant:

 
Sélectionnez
[Test]
public void Test_GetAllSupplier() {

    MockRepository mocks = new MockRepository();

    ISuppliersDataAccess da = mocks.CreateMock<ISuppliersDataAccess>();

    DataTable dt = new DataTable();
    dt.Columns.Add("SupplierID", typeof(int));
    dt.Columns.Add("CompanyName", typeof(string));
    dt.Columns.Add("ContactName", typeof(string));
    dt.Columns.Add("City", typeof(string));
    dt.Columns.Add("Country", typeof(string));

    dt.Rows.Add(new Object[] {1,"Fournisseur 1","Contact 1","Ville 1","Pays 1"});
    dt.Rows.Add(new Object[] {2,"Fournisseur 2","Contact 2","Ville 2","Pays 2"});

    DataSet ds = new DataSet();
    ds.Tables.Add(dt);

    using (mocks.Record()) {
        Expect
            .Call(da.GetAllSuppliers())
            .Return(ds);
    }

    SuppliersFactory.DataFactory = da;           

        using (mocks.Playback()) {
            List<Suppliers> list = SuppliersFactory.GetAllSuppliers();
            Assert.AreEqual(list.Count, 2);
            Assert.AreEqual(list[0].Supplierid, 1);
            Assert.AreEqual(list[0].Address, string.Empty);
            Assert.AreEqual(list[0].Contactname, "Contact 1");
            Assert.AreEqual(list[0].City, "Ville 1");
            Assert.AreEqual(list[1].Supplierid, 2);
            Assert.AreEqual(list[1].Contactname, string.Empty);
        }
}

En faisant tourner le test la première fois, j'ai une erreur qui remonte. En effet, dans l'implémentation d'origine, mon constructeur de Suppliers n'appelle pas la constructeur par défaut, qui prends en charge l'initialisation. Address vaut donc null, ce qui fait échouer le test. Après une modification du constructeur qui prends un Datarow en paramètre, de façon à ce qu'il appelle le constructeur par défaut, mes tests fonctionnent !

Ces tests sont intéressants, mais ils ne démontrent qu'une seule des fonctionnalités du Framework. En effet, tout ce que j'ai fait jusqu'a présent se résume a utiliser le Framework comme fournisseur de bouchons, et pas réellement comme fournisseur de simulacres.

IV-C. Utilisation de simulacres comme bouchon dynamique

Nos simulacres étant dynamiques, on va pouvoir s'en servir comme des bouchons dynamiques. C'est un peu l'approche que l'on à utilisé précédemment pour simuler l'accès à la base de données.
Un des cas, en dehors des bases de données, ou on va utiliser ce style de test, est le cas ou on a des objets longs a initialiser, ou nécessitant beaucoup de données a l'utilisation, et qui vont être utilise pour fournir des données a d'autres fonctions.
Imaginons que l'on a une interface IPersonne, par exemple.
Cette interface définit tout ce que l'on doit connaître d'une personne dans notre application, et va potentiellement être assez fournie en accesseurs...
Par exemple, on va avoir :

 
Sélectionnez
public Interface IPersonne
{
   string Nom {get; }
   string Prenom {get; }
   string Genre {get; }
   IAdresse AdresseDomicile {get; }
   IAdresse AdresseTravail {get; }
   DateTime DateDeNaissance {get; }
   ITelephone TelephoneDomicile {get; }
   ITelephone TelephoneMobile {get; }
   ITelephone TelephoneTravail {get; }
   // etc, etc...
}

Pour peu que l'on veuille tester une fonction dont le simple but est de formater quelques informations dans le but d'un publipostage, sous la forme de "Joyeux anniversaire, [Genre] [Prenom] [Nom]", un bouchon de test standard va nécessiter l'implémentation de TOUS les accesseurs de IPersonne...pour en tester 3. On va donc appeler notre Framework à la rescousse, et écrire le test suivant :

 
Sélectionnez
[Test]
public void Test_Sujet_Publipostage() {
    MockRepository mocks = new MockRepository();

    IPersonne personne = mocks.CreateMock<IPersonne>();

    using (mocks.Record()) {
        Expect
            .Call(personne.Nom)
            .Return("nom1").Repeat.Any();
        Expect
            .Call(personne.Prenom)
            .Return("prenom1").Repeat.Any();
        Expect
            .Call(personne.Genre)
            .Return("Mr").Repeat.Any();
    }

    using (mocks.Playback()) {
        Assert.AreEqual(
                MailingFactory.GetFormalSubject(personne),
                "Joyeux anniversaire, Mr nom1 prenom1");

        Assert.AreEqual(
                MailingFactory.GetCasualSubject(personne),
                "Joyeux anniversaire, prenom1 nom1");
    }
    
    personne = mocks.CreateMock<IPersonne>();

    using (mocks.Record()) {
        Expect
            .Call(personne.Nom)
            .Return(null).Repeat.Any();
        Expect
            .Call(personne.Prenom)
            .Return("prenom1").Repeat.Any();
        Expect
            .Call(personne.Genre)
            .Return(null).Repeat.Any();
    }

    using (mocks.Playback()) {
        Assert.AreEqual(
                MailingFactory.GetCasualSubject(personne),
                "Joyeux anniversaire, prenom1");

        Assert.AreEqual(
                MailingFactory.GetFormalSubject(personne),
                "Joyeux anniversaire, prenom1");
    }
}

Dans un premier temps, on a ajoute Repeat.Any(), de façon à ne pas avoir à faire trop d'aller-retour entre notre test et notre implémentation, vu que nous ne savons pas réellement, avant d'implémenter nos fonctions, le nombre d'accès que l'on va avoir a faire aux différents variables (c'est aussi surtout, de ma part, par fainéantise, et parce que je ne suis pas la doctrine TDD a la lettre...). Après quelques itérations, j'arrive au code suivant :

 
Sélectionnez
public static string GetFormalSubject(IPersonne personne) {

        return string.Format("Joyeux anniversaire, {0}{1}{2}",
            personne.Genre != null ? personne.Genre + " " : "",
            personne.Nom != null ? personne.Nom + " " : "",
            personne.Prenom != null ? personne.Prenom + " " : "").Trim();
}

public static string GetCasualSubject(IPersonne personne) {

        return string.Format("Joyeux anniversaire, {0}{1}",
           personne.Prenom != null ? personne.Prenom + " " : "",
           personne.Nom != null ? personne.Nom + " " : "").Trim();
}

Et finalement, pour recoller au comportement de mon implémentation, je vais mettre à jour les attentes de mon test :

 
Sélectionnez
[Test]
public void Test_Sujet_Publipostage() {
    MockRepository mocks = new MockRepository();

    IPersonne personne = mocks.CreateMock<IPersonne>();

    using (mocks.Record()) {
        Expect
            .Call(personne.Nom)
            .Return("nom1").Repeat.Times(4);
        Expect
            .Call(personne.Prenom)
            .Return("prenom1").Repeat.Times(4);
        Expect
            .Call(personne.Genre)
            .Return("Mr").Repeat.Twice();
    }

    using (mocks.Playback()) {
        Assert.AreEqual(
                MailingFactory.GetFormalSubject(personne),
                "Joyeux anniversaire, Mr nom1 prenom1");

        Assert.AreEqual(
                MailingFactory.GetCasualSubject(personne),
                "Joyeux anniversaire, prenom1 nom1");
    }
    
    personne = mocks.CreateMock<IPersonne>();

    using (mocks.Record()) {
        Expect
            .Call(personne.Nom)
            .Return(null).Repeat.Times(2);
        Expect
            .Call(personne.Prenom)
            .Return("prenom1").Repeat.Times(4);
        Expect
            .Call(personne.Genre)
            .Return(null);
    }

    using (mocks.Playback()) {
        Assert.AreEqual(
                MailingFactory.GetCasualSubject(personne),
                "Joyeux anniversaire, prenom1");

        Assert.AreEqual(
                MailingFactory.GetFormalSubject(personne),
                "Joyeux anniversaire, prenom1");
    }
}

Comme ca, mon test n'est pas seulement un super-bouchon dynamique, mais valide aussi l'interaction entre IPersonne et les fonctions de MailingFactory.

Le projet, dans l'état actuel, est disponible ici.

IV-D. Simulation de la classe de log(...ou pas)

Je veux maintenant pouvoir simuler ma classe de log, pour pouvoir vérifier que, dans le cas ou je veux écrire un événement dans mon fichier de log depuis une de mes classes métier, un message est bien écrit dans le log, et qu'il correspond à ce que j'attends. Plutôt que de faire cette vérification directement dans le fichier de log résultant, je veux utiliser mon Framework pour vérifier que la logique d'interaction est respectée.

Comme pour ma classe d'accès aux données, je veux ajouter une interface, puis un moyen d'injecter l'objet simulacre à la place de mon objet de log actuel.... Et la, c'est le drame... Comme je ne veux pas utiliser, dans cet article, de Framework d'injection de dépendance, et que je ne veux pas reprendre toute l'implémentation pour de la classe de log (imaginons le cas d'une application que je dois faire évoluer, et pas d'un nouveau développement), je vais me retrouver coincé...
Coincé ??? Pas si sur...

V. V. Utilisation du Framework TypeMock Isolator

V-A. Qu'est-ce que TypeMock Isolator ?

TypeMock Isolator est un Framework de simulacres dynamique, tout comme Rhino.
Il est développé par une équipe, incluant Eli Lopian(http://www.elilopian.com/), qui à un blog assez intéressant sur les tests unitaires, ainsi que Roy Osherove (http://weblogs.asp.net/rosherove/default.aspx), qui vient de publier un livre sur les tests unitaires. TypeMock est un produit commercial, mais qui offre une version communautaire gratuite (qui est d'ailleurs celle sur laquelle on va se baser).
La version communautaire utilise des chaînes de caractère pour les appels aux fonctions et aux propriétés des classes simulées. Il est à noter que la version commerciale, elle, permet de faire le même genre d'appels que Rhino, ce qui permets de diminuer les risques qu'une refactorisation ne mette en péril les tests (les chaînes de caractère n'étant pas vérifiées par le compilateur...)

La description donnée sur la page d'accueil du site est la suivante

  • Permet de tester le code qui était auparavant 'untestable'
  • Réduit le temps consacré à l'écriture des tests unitaires, en vous donnant plus de temps pour vous concentrer sur vos tâches réelles
  • Permets de gagner du temps en éliminant la refactorisation et la réécriture du code juste pour le rendre testable
  • Encourage les développeurs à écrire des tests unitaires
  • Tout cela est GRATUIT

L'idée de base de TypeMock, est d'utiliser la programmation orientée aspect pour rediriger les appels au code vers les parties simulées. Contrairement à un Framework de simulacres standard, TypeMock ne nécessite pas d'utiliser un style de développement spécifique pour fonctionner. En fait, il permet tout simplement de se passer de framework d'injection de dépendance, et de ne pas avoir à surcharger chaque classe à tester par une interface.

Ceci est vrai SEULEMENT si la seule raison que l'on avait d'utiliser un framework de DI ou des interfaces était pour rendre le code testable. Mon expérience personnelle est que, si les tests unitaires sont la seule raison d'ajouter ces éléments, alors autant les enlever que de devoir maintenir un niveau d'indirection dont l'utilité finale est réduite. Le fait d'utiliser TypeMock ne doit pas non plus inciter à coder du code non maintenable.
Le but, au final, est d'arriver à équilibrer la lisibilité, la testabilité et la maintenabilité du code.

V-B. Modifier mes tests existants

Pour ne pas avoir à maintenir les deux Frameworks, je vais déjà mettre à jour le code de mes tests existants, en utilisant TypeMock au lieu de Rhino.

 
Sélectionnez
public void Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data() {

    MockRepository mocks = new MockRepository();

    ISuppliersDataAccess da = mocks.CreateMock<ISuppliersDataAccess>();

    using (mocks.Record()) {
        Expect
            .Call(da.GetAllSuppliers())
            .Return(new DataSet());
        Expect
            .Call(da.GetAllSuppliers())
            .Return(null);
    }

    SuppliersFactory.DataFactory = da;

    using (mocks.Playback()) {
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
    }
}

Le test va devenir :

 
Sélectionnez
public void Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data()
{
    MockManager.Init();
        Mock mock = MockManager.MockAll<SuppliersDataAccess>();
        mock.ExpectAndReturn("GetAllSuppliers", new DataSet());
        mock.ExpectAndReturn("GetAllSuppliers", null);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        MockManager.Verify();
}

Les premières et dernières lignes du code permettent d'initialiser et de vérifier le conteneur de simulacres. Je les ai mises ici pour l'exemple, mais en réalité, on les déplacera dans les appels aux fonctions SetUp et TearDown de Nunit. L'appel à Verify permets de vérifier que les attentes définies dans ExpectAndReturn sont remplies par notre code.

V-C. Simuler (enfin) la classe de log

Pour pouvoir avoir un test, je vais modifier la fenêtre d'affichage de ma liste de fournisseurs. Imaginons que je veuilles que les appels à la routine qui liste les fournisseurs soient loggés...ainsi que les exceptions... Je vais modifier mon code dans ma fenêtre, pour lui donner la forme suivante :

 
Sélectionnez
List<Suppliers> suppliers;
try
{
        LogFactory.LogDebug("Récupération de la liste des fournisseurs");
        suppliers = SuppliersFactory.GetAllSuppliers();
}
catch (Exception ex)
{
        LogFactory.LogDebug("Erreur dans GetAllSuppliers : " + ex.Message);
        return;
}

Je vais maintenant faire le test suivant :

 
Sélectionnez
[Test]
public void Test_Logs()
{
    Mock mockLog = MockManager.MockAll(typeof(LogFactory));
    Mock mockFactory = MockManager.MockAll(typeof(SuppliersFactory));

    mockFactory
        .ExpectAndThrow("GetAllSuppliers", new Exception("test"));
    mockLog
        .ExpectCall("LogDebug")
        .Args("Récupération de la liste des fournisseurs");
    mockLog
        .ExpectCall("LogDebug")
        .Args("Erreur dans GetAllSuppliers : test");

    FrmListeFournisseurs frm = new FrmListeFournisseurs();
    
    mockFactory
         .ExpectAndReturn("GetAllSuppliers", new List<Suppliers>());
    mockLog
        .ExpectCall("LogDebug")
        .Args("Récupération de la liste des fournisseurs");

    frm = new FrmListeFournisseurs();
}

Le projet, dans l'état actuel, est disponible ici.

VI. VI. Autres frameworks de simulacres

Pour finir ce petit tour d'horizon, je vais vous présenter deux autres frameworks de simulacres.

VI-A. NMock

NMock est un peu l'ancêtre des autres Framework de simulacres en .net.

En effet, c'est (à ma connaissance), non seulement un des plus vieux de ces Frameworks encore actif, mais en plus le premier qui ne soit pas un portage de Framework depuis Java.
C'est un projet open source, dont la page d'accueil se trouve ici. La grosse différence entre Rhino et NMock est que NMock utilise la même approche que TypeMock, à savoir l'utilisation de chaînes pour l'appel de la fonction simulée. Par exemple, un des tests précédemment écrit (Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data) se traduirait, en NMock, par :

 
Sélectionnez
[Test]
public void Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data() {

    Mockery mocks = new Mockery ();
    ISuppliersDataAccess da = mocks.NewMock<ISuppliersDataAccess>();

    Expect
        .Once.On(da)
        .Method("GetAllSuppliers")
        .Will(Return.Value(new DataSet()));
    Expect
        .Once.On(da)
        .Method("GetAllSuppliers")
        .Will(Return.Value(null));

    SuppliersFactory.DataFactory = da;

    Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
    Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);       
    mocks.VerifyAllExpectationsHaveBeenMet();
}

VI-B. Moq

Moq est un nouveau Framework de simulacres, qui se base sur les capacités du Framework 3.5.

Il a été développé pour utiliser à son avantage LINQ et les expressions lambda, ce qui en fait, d'après ses concepteurs, le Framework le plus productif, simple, et le plus facile à refactoriser. A noter, tout comme TypeMock, il peut indifféremment simuler des classes ou des interfaces.

De la même façon que Rhino, il doit par contre se baser sur une injection de code pour pouvoir fonctionner.
Pour l'exemple, voici la version Moq de Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data :

 
Sélectionnez
[Test]
public void Test_GetAllSupplier_Returns_Empty_When_Null_Or_No_Data() {
        
        var da = new Mock<SuppliersDataAccess>();  
        da.Expect(u => u.GetAllSuppliers()).Returns(new DataSet());
        da.Expect(u => u.GetAllSuppliers()).Returns(null);
        
        SuppliersFactory.DataFactory = da.Object;
        
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);
        Assert.AreEqual(SuppliersFactory.GetAllSuppliers().Count, 0);    
}

On retrouve une syntaxe plus "légère" à l'utilisation. En effet, les attentes du simulacre sont "embarquées" dans l'objet simulacre lui-même. C'est d'ailleurs pour cette raison que l'on doit passer a la Factory da.Object, et non pas da, l'objet da étant effectivement un adaptateur du simulacre de SuppliersDataAccess.
Pour plus d'informations, je vous redirige sur la page de démarrage rapide de Moq.

VII. VII. Aller plus loin

Je ne suis, personnellement, pas un gourou des tests unitaires ou du TDD, plutôt un amateur convaincu.

Pour ceux qui voudraient approfondir le sujet des tests unitaires, je vous conseille de lire les traductions et articles de Bruno Orsier sur developpez, en Français.
Pour approfondir les simulacres, je vous conseille de...vous armer de patience, et de partir pour une longue quête qui vous amènera probablement sur un (voire plusieurs) champs de batailles de religion.
En effet, l'un dans l'autre, les Frameworks sont assez récents (EasyMock, l'ancêtre, date de 2001 pour sa version Java), et les membres les plus actifs des différents projets sont souvent des "extrémistes" du développement objet, avec des avis bien tranchés...

Un bon point de départ, à mon humble avis, est de parcourir les docs des différents Frameworks, en commençant par celle de Rhino, qui est la plus mature, et de pratiquer dés que possible.

VIII. VII. Conclusion

Les Frameworks de simulacres permettent d'affiner la granularité des tests unitaires d'une application, en vérifiant que les objets réagissent convenablement dans un contexte très contrôlé.
Ils permettent très facilement de valider des interactions, et peuvent même aller, dans des cas extrêmes, jusqu'à permettre de commencer le développement d'une application sans base de données, ou sans certains composants, qui seront simulés dynamiquement.
Ceci dit, ils ne sont pas une panacée. En particulier, avant de simuler un objet, il est conseillé d'évaluer si il ne serait pas plus bénéfique de refactoriser le code...

Par exemple, prenons le cas d'une fonction se basant sur la validation d'une date.

 
Sélectionnez
public static bool IsAfternoon(){
        return DateTime.Now.Hour > 12;
}

On pourrait imaginer mettre en place une interface IDateProvider, avec une fonction ou une propriété GetDate, qui retournerait, dans le cas réel, la date courante, et, dans le cas des tests, une date choisie par le développeur.
Il serait certainement plus judicieux, dans ce cas, de refactoriser ainsi la fonction.

 
Sélectionnez
public static bool IsAfternoon(DateTime dateToTest){
  return dateToTest.Hour > 12;
}

Une autre erreur à ne pas faire est que, si nous avons deux objets A et B, et que nous simulons l'objet A pour tester l'objet B, on va effectivement tester l'objet B, et l'interaction entre A et B, mais pas notre objet A...

Pour conclure la conclusion, j'ai utilise exclusivement Rhino pendant quelques mois, avant de basculer sur TypeMock récemment. En effet, dans l'environnement ou je travaille, maintenir :

 
Sélectionnez
File.WriteAllText(filename, text);

reste plus facile que de maintenir :

 
Sélectionnez
using(TextWriter writer = IoC.Resolve<IFileWriterFactory>().Create(filename))
        writer.Write(text);

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Philippe Vialatte. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.