Introduction à Moles, plugin d’isolation

2 February 2012 par richar_o

Les méthodologies comme l’Extreme Programming ou le Test Driven Development ont favorisé l’essor et l’adoption des tests unitaires. Mais il n’est pas toujours possible de rendre ses tests réellement unitaires, quand le code souffre de dépendances externes, comme une base de données, une connexion réseau, … C’est dans ce genre de situations qu’interviennent les frameworks d’isolation comme Moq ou RhinoMocks. Microsoft a, de son coté, développé Moles, un plugin Visual Studio qui poursuit le même but.

Principe de l’isolation

L’exemple typique illustrant la nécessité d’un framework d’isolation implique des dates. Considérons l’extrait de code suivant :

public DateTime Today()
{
	return DateTime.Now;
}

Comment tester cette méthode, étant donné que son résultat change à chaque exécution ? C’est là qu’intervient Moles, en nous permettant de redéfinir la propriété DateTime.Now le temps d’un test unitaire.

public void TestToday()
{
        MDateTime.NowGet = () => new DateTime(2010, 5, 30);
	Assert.AreEqual(Today(), new DateTime(2010, 5, 30));
}

Évidemment, l’exemple est fictif et a ici peu d’intérêt.

Moles est capable, comme dans cet exemple, de détourer une méthode ou une propriété existante, mais aussi de générer des ébauches (“stubs”) d’interface ou de classes abstraites. C’est par cet aspect que commencera ce tutoriel.

Mocking d’interfaces

Le projet à tester sera une bibliothèque de classes C# appelée Library, contenant une interface et une classe définies comme suit :

public interface IFileSystem
{
    string ReadAllText(string fileName);
}
 
public class Parser
{
    public bool Parse(IFileSystem fs, string fileName)
    {
        return fs.ReadAllText(fileName) == “OK”;
    }
}

La classe Parser dépend donc d’un IFileSystem, passé en argument de la méthode Parse. C’est une interprétation rudimentaire de l’Inversion de Contrôle (IoC), mais qui illustre bien l’exemple.
La méthode Parse s’attend donc à ce que le contenu du fichier soit “OK”. Or, cette méthode peut donc échouer à cause d’un fichier absent ou corrompu, un système de fichiers sur le réseau en panne, … Quand tout ce que l’on veut tester pour l’instant, c’est le bon fonctionnement du “Parser”. Moles va donc isoler cette classe en créant une ébauche de la classe IFileSystem.

Pour cela, demandons à Visual Studio de générer un projet de test et le test de la méthode Parse. Le code résultant, après quelques modifications mineures, doit ressembler à ça :

        ///
        /// On considère le fichier d'entrée comme valide : Parse doit donc retourner true
        ///
        [TestMethod()]
        public void ParseTest()
        {
            Parser target = new Parser();
            IFileSystem fs = null; // Null pour l'instant
            string fileName = string.Empty; // peu importe ici
            bool expected = true;
            bool actual = target.Parse(fs, fileName);
            Assert.AreEqual(expected, actual);
        }

Il faut maintenant indiquer à Moles que nous souhaitons des ébauches des types déclarés dans l’assembly “Library”. Pour cela, dans la liste des références du projet de test, il suffit de sélectionner “Add Moles Assembly” dans le menu contextuel de la référence à Library.
Il suffit d’ajouter le using correspondant : “using Library.Moles”, pour pouvoir utiliser les ébauches de Moles. Attention, ces ébauches n’existent qu’une fois le projet Library compilé.

Une ébauche, par convention, possède le même nom que le type qu’elle implémente préfixé de “S”. Cette ébauche définit, pour chaque méthode du type originel, une propriété qui est un delegate, afin de redéfinir la méthode :

    IFileSystem fs = new SIFileSystem() { ReadAllTextString = file => "OK" };
    // La méthode renvoie ‘OK’, quelque soit le nom du fichier

Le test passe maintenant.

Mocking de classes

La seconde famille de types isolés est celle des Moles : une Mole est un type préfixé par la lettre M, qui permet de redéfinir les méthodes et propriété du type originel, qui est une classe abstraite ou non.

Cette fois ci, l’exemple est plus concret : ajoutons une base de données locale appelée Database.sdf au projet Library. Le type de modèle est Entity, généré à partir de la base de données, et s’appelle DatabaseEntities.

L’éditeur graphique permet d’ajouter rapidement une table à cette DB : Movies, qui contient deux colonnes id (int, not null, unique, primary key et identity) et title (nvarchar). Il faut ensuite mettre à jour le modèle entity afin qu’il intègre cette nouvelle table.

Il faut ensuite créer une couche d’abstraction : une classe DataAccess, définie comme suit :

namespace Library
{
    public class DataAccess
    {
        private DatabaseEntities entities_;
 
        public DataAccess(DatabaseEntities databaseEntities)
        {
            entities_ = databaseEntities;
        }
 
        public void Add(Movies m)
        {
            entities_.Movies.AddObject(m);
        }
 
        public int Count
        {
            get { return entities_.Movies.Count(); }
        }
    }
}

Et ensuite, un test unitaire :

        [TestMethod()]
        [HostType("Moles")] // Important pour les Moles
        public void AddToMoviesTest()
        {
            //On va simuler la DB sous la forme d’une liste
            List list = new List();
 
	    // Pour toutes les instances de DataAccess:
	    // On redéfinit la méthode AddMovies et la propriété Count
            MDataAccess.AllInstances.AddMovies = (da, m) => list.Add(m);
            MDataAccess.AllInstances.CountGet = (da) => list.Count;
 
	    // Ce DataAccess est impacté par la Mole.
            // Sinon, le paramètre null lèverait une exception.
            DataAccess moledDa = new DataAccess(null);
 
            var oldCount = moledDa.Count;
            moledDa.Add(new Movies());
            var newCount = moledDa.Count;
 
            Assert.AreEqual(oldCount + 1, newCount);
        }

Il est aussi possible de redéfinir une méthode ou une propriété pour une instance particulière, mais aussi les méthodes statiques, les constructeurs, …

A noter, l’attribut [HostType(“Moles”)], qui précise au framework de tests unitaires de VS de lancer chacun des tests dans le contexte de Moles, afin que puisse s’effectuer l’instrumentation de code. En effet, Moles intercepte les appels aux méthodes et propriétés pour leur substituer les lambda expressions définies par le développeur.

Moles est donc un outil puissant, qui permet une isolation de code appréciable dans le cadre de tests unitaires. De plus, le mocking s’étend des interfaces aux classes abstraites, des constructeurs aux méthodes statiques : ces possibilités peuvent être utiles à tout projet de testing, afin de maximiser l’efficience des tests unitaires.

Tags: , , , ,

Laisser un commentaire