web-dev-qa-db-fra.com

Meilleure façon de tester des méthodes unitaires qui appellent d'autres méthodes dans la même classe

J'ai récemment discuté avec des amis de laquelle des 2 méthodes suivantes est préférable de stub renvoyer les résultats ou les appels aux méthodes à l'intérieur de la même classe à partir de méthodes à l'intérieur de la même classe.

Ceci est un exemple très simplifié. En réalité, les fonctions sont beaucoup plus complexes.

Exemple:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Donc, pour tester cela, nous avons 2 méthodes.

Méthode 1: utilisez des fonctions et des actions pour remplacer la fonctionnalité des méthodes. Exemple:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Méthode 2: rendre les membres virtuels, dériver la classe et dans la classe dérivée utiliser des fonctions et des actions pour remplacer la fonctionnalité Exemple:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Je veux savoir ce qui est mieux et aussi POURQUOI?

Mise à jour: REMARQUE: FunctionB peut également être public

36
tranceru1

Modifié après la mise à jour de l'affiche originale.

Avertissement: pas un programmeur C # (principalement Java ou Ruby). Ma réponse serait: je ne le testerais pas du tout, et je ne pense pas que vous devriez.

La version plus longue est la suivante: les méthodes privées/protégées ne font pas partie de l'API, ce sont essentiellement des choix d'implémentation, que vous pouvez décider de revoir, de mettre à jour ou de supprimer complètement sans aucun impact sur l'extérieur.

Je suppose que vous avez un test sur FunctionA (), qui est la partie de la classe visible du monde extérieur. Ce devrait être le seul à avoir un contrat à mettre en œuvre (et qui pourrait être testé). Votre méthode privée/protégée n'a aucun contrat à remplir et/ou à tester.

Voir une discussion connexe ici: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Après le commentaire , si FunctionB est public, je vais simplement tester les deux en utilisant le test unitaire. Vous pouvez penser que le test de FunctionA n'est pas totalement "unitaire" (comme il s'appelle FunctionB), mais je ne serais pas trop inquiet par cela: si le test FunctionB fonctionne mais pas le test FunctionA, cela signifie clairement que le problème n'est pas dans le sous-domaine de FunctionB, ce qui est assez bon pour moi en tant que discriminateur.

Si vous voulez vraiment être en mesure de séparer totalement les deux tests, j'utiliserais une sorte de technique de simulation pour se moquer de FunctionB lors du test de FunctionA (généralement, retourne une valeur correcte connue fixe). Je n'ai pas les connaissances de l'écosystème C # pour conseiller une bibliothèque de moquerie spécifique, mais vous pouvez regarder cette question .

32
Martin

Je souscris à la théorie selon laquelle si une fonction est importante à tester ou importante à remplacer, elle est suffisamment importante pour ne pas être un détail d'implémentation privé de la classe testée, mais pour être un détail d'implémentation publique d'un différent classe.

Donc, si je suis dans un scénario où j'ai

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Ensuite, je vais refactoriser.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Maintenant, j'ai un scénario où D() est testable indépendamment et entièrement remplaçable.

En tant que moyen d'organisation, mon collaborateur pourrait ne vit pas au même niveau d'espace de noms. Par exemple, si A se trouve dans FooCorp.BLL, alors mon collaborateur peut avoir une autre couche de profondeur, comme dans FooCorp.BLL.Collaborators (ou tout autre nom approprié). Mon collaborateur pourrait en outre être uniquement visible à l'intérieur de l'assembly via le modificateur d'accès internal, que j'exposerais ensuite à mes projets de test unitaire via l'attribut InternalsVisibleTo Assembly. La conclusion est que vous pouvez toujours garder votre API propre, en ce qui concerne les appelants, tout en produisant du code vérifiable.

11
Anthony Pegram

Personnellement, j'utilise la méthode 1, c'est-à-dire la transformation de toutes les méthodes en actions ou en fonctions, car cela a énormément amélioré la testabilité du code pour moi. Comme pour toute solution, cette approche présente des avantages et des inconvénients:

Avantages

  1. Permet une structure de code simple où l'utilisation de modèles de code uniquement pour les tests unitaires peut ajouter une complexité supplémentaire.
  2. Permet de sceller les classes et d'éliminer les méthodes virtuelles requises par les frameworks de moquerie populaires comme Moq. Le scellement des classes et l'élimination des méthodes virtuelles en font des candidats pour les optimisations intégrées et autres compilateurs. ( https://msdn.Microsoft.com/en-us/library/ff647802.aspx )
  3. Simplifie la testabilité car le remplacement de l'implémentation Func/Action dans un test unitaire est aussi simple que l'attribution d'une nouvelle valeur à Func/Action
  4. Permet également de tester si un Func statique a été appelé à partir d'une autre méthode car les méthodes statiques ne peuvent pas être moquées.
  5. Facile à refactoriser les méthodes existantes en Funcs/Actions car la syntaxe pour appeler une méthode sur le site d'appel reste la même. (Pour les moments où vous ne pouvez pas refactoriser une méthode en Func/Action, voir les inconvénients)

Les inconvénients

  1. Les Funcs/Actions ne peuvent pas être utilisés si votre classe peut être dérivée car les Funcs/Actions n'ont pas de chemins d'héritage comme les méthodes
  2. Impossible d'utiliser les paramètres par défaut. La création d'un Func avec des paramètres par défaut implique la création d'un nouveau délégué qui pourrait rendre le code déroutant selon le cas d'utilisation
  3. Impossible d'utiliser la syntaxe de paramètre nommée pour appeler des méthodes comme quelque chose (firstName: "S", lastName: "K")
  4. Le plus gros inconvénient est que vous n'avez pas accès à la référence "this" dans Funcs et Actions, et donc toutes les dépendances sur la classe doivent être explicitement transmises en tant que paramètres. C'est bon car vous connaissez toutes les dépendances mais mauvais si vous avez de nombreuses propriétés dont dépendra votre Func. Votre kilométrage variera en fonction de votre cas d'utilisation.

Donc, pour résumer, utiliser Funcs et Actions pour le test unitaire est génial si vous savez que vos classes ne seront jamais remplacées.

De plus, je ne crée généralement pas de propriétés pour Funcs, mais je les insère directement en tant que telles

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

J'espère que cela t'aides!

0
Salman Hasrat Khan

Pour ajouter à ce que Martin souligne,

Si votre méthode est privée/protégée - ne la testez pas. Il est interne à la classe et ne doit pas être accessible en dehors de la classe.

Dans les deux approches que vous mentionnez, j'ai ces préoccupations -

Méthode 1 - Cela modifie en fait le comportement de la classe sous test dans le test.

Méthode 2 - En fait, cela ne teste pas le code de production, mais teste une autre implémentation.

Dans le problème énoncé, je vois que la seule logique de A est de voir si la sortie de FunctionB est paire. Bien qu'illustratif, FunctionB donne une valeur aléatoire, ce qui est difficile à tester.

Je m'attends à un scénario réaliste où nous pouvons configurer MyClass de telle sorte que nous sachions ce que FunctionB retournerait. Ensuite, notre résultat attendu est connu, nous pouvons appeler FunctionA et affirmer le résultat réel.

0
Srikanth Venugopalan