Dummy, Fake, Stub, Mock et Spy, les tests unitaires avec l’aide de Moq.

J’ai travaillé dans plusieurs sociétés et encore plus d’équipes différentes. Souvent, on m’a parlé de tests unitaires, que c’était important pour la stabilité et la robustesse de la solution.
Je me suis rendu compte qu’il s’agissait d’un abus de langage : la plupart du temps, j’étais devant des tests d’intégration. Cette erreur avait très souvent la même explication : les équipes n’avaient pas correctement isolé leur code.

L’isoler veut dire permettre à son applicatif de s’affranchir de ses dépendances externes (sa base de données par exemple).
Je vais ici tenter d’expliquer les différentes techniques permettant de couper ou simuler ces dépendances.

Les personnes qui pourraient être intéressées par cet article sont principalement (mais pas seulement) : 

  • Les développeurs débutants dans les tests unitaires.
  • Ceux ayant déjà survolé le sujet voulant approfondir leurs connaissance et compréhension.

Je risque pour les second d’enfoncer quelques portes ouvertes. Mais il est toujours bon de rappeler les bases.

Que sont les doublures de test ?

Durant le développement de mon application, j’aurai sûrement à implémenter à un moment donné des tests unitaires.
Chaque test doit être atomique et ne tester que son état. Pas celui d’autres méthodes ou objets. Je ne dois, par exemple, jamais atteindre la base de données ou le système de fichier. Je passerai alors dans l’univers des tests d’intégration.
Du coup, je dois faire croire à mon code soit que j’appelle bien la méthode qu’il m’a demandé d’utiliser soit que l’objet dont il peut avoir besoin existe.
Cet ensemble de subterfuges s’appellent des doublures de test. Ceux-ci sont identifiés et nommés comme suit : Dummy, Fake, Stub, Mock, Spy.
Leurs implémentations peuvent aller du trivial au casse-tête si je n’utilise pas d’outil spécifique existant.
C’est pourquoi j’utiliserai la bibliothèque Moq pour illustrer mes exemples.
A noter que si en général, l’utilité des doublures de test est avéré pendant les tests unitaires; je peux dans certains cas précis utiliser leur principe ailleurs dans mon projet.

Définitions et implémentations

Dummy

Il s’agit de faire croire à ma destination que l’objet existe. Celui-ci ne fera rien, il ne contiendra rien non plus. Mais il existera aux yeux de mon code testé.
Exemple : Admettons que nous ayons une classe que nous appellerons MasterClass. Son constructeur unique prend en paramètre une implémentation d’une interface lui servant de DAL.
Nous avons un test à faire : qu’un GUID soit bien généré suite à l’appel du constructeur. Ce test n’a pas besoin de mon appel à la DAL.
Alors soit, je peux passer null au constructeur :
[TestMethod]
public void MasterClass_Contient_Guid()
{
    var master = new MasterClass(null);
    Assert.IsNotNull(master.Guid);
}
Si d’aventure mon constructeur appelle ma DAL directement, mon test ne passera jamais (le constructeur renverra invariablement une NullReferenceException).
Je peux me servir de Moq afin de faire croire à mon constructeur que si, je lui ai bien donné une instance de l’interface :
[TestMethod]
public void MasterClass_Contient_Guid()
{
    // Initialisation des variables utilisant Moq
    var repo = new Mock<IDataRepository>();

    IDataRepository dummyRepo = repo.Object;

    // On appelle le constructeur avec le dummy
    var master = new MasterClass(dummyRepo);

    // Enfin, on effectue une assertion qui n'a pas besoin
    // d'utiliser IDataRepository.
    Assert.IsNotNull(master.Guid);
}
Attention : avec Moq, l’appel à une méthode d’un Dummy renverra toujours null. Gare à vos validations !

Fake

Un Fake est une implémentation simplifiée d’un comportement attendu. Ici, on ne fait pas croire à notre code quelque chose; on l’a implémenté explicitement. Il sera juste simplifié et ne fera appel à aucune dépendance.
Je n’ai pas besoin de Moq pour cette doublure de test. Je dois créer une classe qui me servira uniquement pour mes tests.
public class FakeDataAccessLayer : IDataRepository
{
    private int nbInstances;
    public FakeDataAccessLayer(int nbInstances)
    {
        this.nbInstances = nbInstances;
    }

    List<SlaveClass> IDataRepository.GetSlaves()
    {
        var output = new List<BusinessLayer.SlaveClass>();

        for(int i = 0; i < this.nbInstances; i++)
            output.Add(new SlaveClass() { Id = i, Name = i.ToString() });

        return output;
    }

    int IDataRepository.Save(MasterClass masterClass)
    {
        throw new NotImplementedException();
    }

    int IDataRepository.Save(SlaveClass slaveClass)
    {
        throw new NotImplementedException();
    }
}
Bien évidemment, je n’implémente que ce dont j’ai besoin. Si mon Fake ne teste jamais les méthodes de sauvegarde, il est même préférable de laisser un renvoi d’exception. Ça me rappellera que ce n’est pas normal de passer par cette méthode.
J’utiliserai le Fake de cette manière dans les tests unitaires :
[TestMethod]
public void MasterClass_NbSlaves_Retourne_Liste_Count()
{
    int nbInstances = 3;
    var fakeRepo = new FakeDataAccessLayer(nbInstances);

    var master = new MasterClass(fakeRepo);

    Assert.AreEqual(nbInstances, master.NbSlaves);
}
Le Fake peut également être très utile en dehors des tests unitaires :
Vous commencez un nouveau développement. Vous n’avez aucune information concernant le mode de stockage de vos données.
Cela peut être voulu (Clean Architecture) ou pas (on vous lance sur le développement alors que tout le monde n’est pas encore d’accord sur ce point, au moins).
En utilisant un Fake, vous vous affranchissez des accès extérieurs. Vous pouvez commencer à coder sans attendre de décision supplémentaire. Il ne vous restera enfin qu’à changer l’implémentation à passer à votre appli / site.

Stub

Je reprends le principe du Fake, mais je vais être plus fainéant. Il n’y aura aucune implémentation explicite, Moq me sera utile dans ce cas. Je ne présenterai que ce dont j’ai besoin.
[TestMethod]
public void MasterClass_Save_Met_A_Jour_Id()
{
    // Initialisation des variables utilisant Moq
    var repo = new Mock<IDataRepository>();

    repo.Setup(m => m.Save(It.IsAny<MasterClass>())).Returns(5);

    IDataRepository stubRepo = repo.Object;

    // On appelle le constructeur avec le stub
    var master = new MasterClass(stubRepo);

    master.Save();

    Assert.AreEqual(5, master.Id);
}
La méthodeSetup(Expression<Action<T>>) de Moq me permet de créer une implémentation de la méthode Save(MasterClass) de IDataRepository.
Dans ce cas précis, je demande à Moq :

« Lorsque tu recevras un appel à la méthode Save(MasterClass). Quelle que soit la valeur du paramètre masterClass (It.IsAny<MasterClass>()), renvoie toujours la valeur 5. »

Rappel : Je ne veux pas tester le retour de la fonction Save(MasterClass) de IDataRepository, je veux m’assurer que la méthode Save(MasterClass) de MasterClass utilise bien le retour de cette première.

Mock

Il est parfois compliqué d’aller vérifier le comportement d’une méthode. Notamment lorsque celle-ci va manipuler d’autres objets ou bien appeler leurs méthodes.

On parle également de « behaviour verification« , expression un peu plus facile à comprendre.

J’utiliserai le Mock le plus souvent pour vérifier une propagation.

Ce qu’il faut garder à l’esprit lorsque l’on parle de Mock, c’est que je vérifie des états et des valeurs que je maîtrise et connais. Si je n’en ai pas le pouvoir (ou si ça n’est pas mon problème), alors je parlerai de Spy.
Admettons qu’au moment de la sauvegarde, si une exception est levée, je dois l’enregistrer dans un fichier quelconque. Celui-ci contiendra la valeur du message de l’exception.
[TestMethod]
public void MasterClass_Save_Enregistre_Message_Exception()
{
    // Initialisation des variables utilisant Moq
    var repo = new Mock<IDataRepository>();
    var fileLogger = new Mock<IFileAccess>();

    repo.Setup(r => r.Save(It.IsAny<MasterClass>())).Throws(new Exception("TestException"));

    var master = new MasterClass(repo.Object);
    master.SetFileLogger(fileLogger.Object);

    master.Save();

    fileLogger.Verify(f => f.LogMessage("TestException"));
}
J’utilise encore la méthode Setup(Expression<Action<T>>) de Moq. Ici, je lui demande de me renvoyer invariablement une exception. Exception dont je connais le message, puisque c’est moi qui le passe.
Arrive la fonction Verify(Expression<Action<T>>, Times). Dans ce cas, je demande à Moq de vérifier que j’ai bien appelé la méthode LogMessage(string) avec le paramètre d’entré égal à « TestException ».
Dans ce cas, je peux avoir appelé la méthode LogMessage(string) n fois pendant mon traitement. La seule chose qui m’intéresse, c’est de savoir si au moins une fois, mon paramètre était « TestException ».

Spy

Le Spy est un dérivé du Mock. Cependant, si dans ce dernier, je vérifiais les paramètres, ici, je m’assure uniquement que je suis passé (ou non) par un chemin ou une méthode.
Reprenons le test précédent et tournons le en Spy :
[TestMethod]
public void MasterClass_Save_Enregistre_Message_Exception_Spy()
{
    // Initialisation des variables utilisant Moq
    var repo = new Mock<IDataRepository>();
    var fileLogger = new Mock<IFileAccess>();

    repo.Setup(r => r.Save(It.IsAny<MasterClass>())).Throws(new Exception());

    var master = new MasterClass(repo.Object);
    master.SetFileLogger(fileLogger.Object);

    master.Save();

    fileLogger.Verify(f => f.LogMessage(It.IsAny<string>()), Times.AtLeastOnce());
}
Le code du test est quasiment le même.
Les seules différences sont que l’exception ne contient plus de message explicite et que Verify(Expression<Action<T>>, Times) ne s’intéresse plus à la valeur du paramètre passé à LogMessage(string).La raison est que dans le Spy, les valeurs passées ne m’intéressent pas. Soit parce que je ne les maîtrise pas soit parce qu’elles ne sont pas au cœur de ma problématique.
J’ai également rajouté Times.AtLeastOnce(), de manière assez évidente, vous aurez compris que je veux vérifier que j’ai appelé « au moins une fois » la méthode. Je peux bien évidemment demander un nombre fixe de fois.

Quelle est la différence fondamentale avec Mock ? Rien ne m’assure que dans le Spy, j’ai bien appelé LogMessage(string) depuis l’endroit du code que je veux valider.

En plus clair : prenons ma méthode Save(MasterClass). Admettons que j’appelle LogMessage(string) autrepart ;pour logger les valeurs de mes paramètres envoyés à IDataRepository par exemple. Et que, par contre, je n’appelle pas ou plus cette méthode lorsqu’une exception est levée… Alors Spy considèrera mon test unitaire en « Réussite » : j’ai correctement appelé LogMessage(string) au moins une fois. A l’inverse, Mock le marquera en « Échec » car je n’ai pas validé la valeur du paramètre passé.
J’espère vous avoir suffisamment présenté les différences, points communs et subtilités des doublures de test. Le sujet est assez complexe. De plus, je me suis rendu compte pendant cette rédaction que je confondais certains d’entre eux. Si j’ai pu éclairer certains d’entre vous, ce serait une excellente chose.
Sources :
Mocks Aren’t Stubs (Martin Fowler)

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s