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


35

Je discutais récemment avec des amis de la meilleure des deux méthodes suivantes pour renvoyer les résultats ou les appels de méthodes dans la même classe à partir de méthodes dans 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 une classe et dans une classe dérivée, utiliser Fonctions et actions pour remplacer des fonctionnalités 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 qui est meilleur et aussi POURQUOI?

Mise à jour: NOTE: FunctionB peut aussi être public


Votre exemple est simple, mais pas tout à fait correct. FunctionAretourne un bool mais ne définit qu'une variable locale xet ne retourne rien.
Eric P.

1
Dans cet exemple particulier, FunctionB peut être public staticmais dans une classe différente.

Pour la révision du code, vous devez publier le code réel et non une version simplifiée de celui-ci. Voir la FAQ. Dans l'état actuel des choses, vous posez une question spécifique qui ne cherche pas à réviser le code.
Winston Ewert

1
FunctionBest cassé par la conception. new Random().Next()est presque toujours faux. Vous devriez injecter l'instance de Random. ( Randomc'est aussi une classe mal conçue, ce qui peut causer quelques problèmes supplémentaires)
CodesInChaos 27/02/2012

sur une note plus générale DI par les délégués est absolument parfait IMHO
jk.

Réponses:


32

Édité 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 le faire.

La version la 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 réviser, de mettre à jour ou de supprimer, 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. Il 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 pas de 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 cela ne me préoccupe pas trop: si le test FunctionB fonctionne mais pas le test FunctionA, cela signifie clairement que le problème ne se trouve pas dans la liste sous-domaine de FunctionB, ce qui est assez bon pour moi en tant que discriminateur.

Si vous voulez vraiment pouvoir séparer totalement les deux tests, je voudrais utiliser une sorte de technique de moquage pour se moquer de FunctionB lors du test de FunctionA (en général, renvoyer une valeur correcte connue fixe). Il me manque la connaissance de l'écosystème C # pour conseiller une bibliothèque moqueuse spécifique, mais vous pouvez vous pencher sur cette question .


2
Tout à fait d’accord avec la réponse de @Martin. Lorsque vous écrivez des tests unitaires pour la classe, vous ne devez pas tester les méthodes . Ce que vous testez est un comportement de classe , que le contrat (la déclaration que la classe est censée faire) est satisfait. Ainsi, vos tests unitaires doivent couvrir toutes les exigences exposées pour cette classe (à l'aide de méthodes / propriétés publiques), y compris les cas exceptionnels

Bonjour, merci pour la réponse, mais cela n'a pas répondu à ma question. Peu importe que FunctionB soit privé / protégé. Il peut également être public et toujours appelé à partir de FunctionA.

Le moyen le plus courant de gérer ce problème, sans MyClassmodifier la conception de la classe de base, consiste à sous-classer et à remplacer la méthode par la fonctionnalité que vous souhaitez remplacer. Il pourrait également être judicieux de mettre à jour votre question pour y inclure des informations FunctionBpubliques.
Eric P.

1
protectedles méthodes font partie de la surface publique d'une classe, sauf si vous vous assurez qu'il ne peut y avoir d'implémentation de votre classe dans des assemblys différents.
CodesInChaos

2
Le fait que FunctionA appelle FunctionB est un détail non pertinent du point de vue des tests unitaires. Si les tests pour FunctionA sont écrits correctement, il s'agit d'un détail d'implémentation qui peut être refactorisé ultérieurement sans interrompre les tests (tant que le comportement général de FunctionA reste inchangé). Le vrai problème est que l'extraction d'un nombre aléatoire par FunctionB doit être effectuée avec un objet injecté, de sorte que vous puissiez utiliser une simulation pendant le test pour vous assurer qu'un nombre connu est renvoyé. Cela vous permet de tester des entrées / sorties connues.
Dan Lyons

11

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

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 refactorer.

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 peut ne pas vivre au même niveau d’espace de nommage. Par exemple, si Aest dans FooCorp.BLL, mon collaborateur peut être une couche plus profonde, comme dans FooCorp.BLL.Collaborators (ou le nom approprié). Mon collaborateur peut également être visible à l'intérieur de l'assembly via le internalmodificateur d'accès, que je voudrais également exposer à mes projets de test d'unité via l' InternalsVisibleToattribut d'assembly. La conclusion est que vous pouvez toujours garder votre API propre en ce qui concerne les appelants tout en produisant un code vérifiable.


Oui si ICollaborator a besoin de plusieurs méthodes. Si vous avez un objet dont le seul travail consiste à envelopper une seule méthode, je préférerais toutefois le voir remplacé par un délégué.
jk.

Vous devez décider si un délégué nommé a un sens ou si une interface a un sens, et je ne déciderai pas pour vous. Personnellement, je ne suis pas opposé aux classes de méthode simples (publiques). Le plus petit, mieux c'est, car ils deviennent de plus en plus faciles à comprendre.
Anthony Pegram

0

Ajoutant à 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 avez mentionnées, j'ai ces préoccupations ...

Méthode 1 - Cela change réellement le comportement de la classe sous test dans le test.

Méthode 2 - Cela ne teste pas le code de production, mais 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 que illustratif, FunctionB donne une valeur aléatoire, difficile à tester.

Je m'attends à un scénario réaliste dans lequel nous pouvons configurer MyClass de manière à connaître le retour de FunctionB. Alors que notre résultat attendu est connu, nous pouvons appeler FunctionA et affirmer le résultat réel.


3
protectedest presque le même que public. Seulement privateet internalsont des détails de mise en œuvre.
CodesInChaos

@codeinchaos - Je suis curieux ici. Pour un test, les méthodes protégées sont "privées" sauf si vous modifiez les attributs d'assembly. Seuls les types dérivés ont accès aux membres protégés. À l'exception du virtuel, je ne vois pas pourquoi protégé devrait être traité comme un public après un test. pourriez-vous élaborer s'il vous plaît?
Srikanth Venugopalan

Comme ces classes dérivées peuvent être dans des assemblys différents, elles sont exposées au code tiers et font donc partie de la surface publique de votre classe. Pour les tester, vous pouvez les créer internal protected, utiliser un assistant de réflexion privé ou créer une classe dérivée dans votre projet de test.
CodesInChaos

@CodesInChaos, a convenu que la classe dérivée peut être dans des assemblys différents, mais que la portée est toujours limitée aux types de base et dérivés. Modifier le modificateur d'accès pour le rendre testable est quelque chose qui me rend un peu nerveux. Je l'ai fait, mais cela me semble être un anti-modèle.
Srikanth Venugopalan

0

Personnellement, j’utilise Method1, c’est-à-dire que toutes les méthodes sont transformées en actions ou en fonctions car cela a considérablement 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 dans laquelle l'utilisation de modèles de code uniquement pour le test unitaire peut ajouter une complexité supplémentaire.
  2. Permet aux classes d'être scellées et d'éliminer les méthodes virtuelles requises par les frameworks moqueurs populaires tels que Moq. Le scellement des classes et l’élimination des méthodes virtuelles en font des candidats à l’optimisation inline et à d’autres optimisations du compilateur. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Simplifie la testabilité, car remplacer l'implémentation Func / Action dans un test unitaire est aussi simple que d'attribuer 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 simulées.
  5. Il est facile de refactoriser les méthodes existantes en fonctions / actions, car la syntaxe permettant d’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 dans un Func / Action, voir ci-contre)

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 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é susceptible de rendre le code confus en fonction du cas d'utilisation.
  3. Impossible d'utiliser la syntaxe du paramètre nommé pour appeler des méthodes comme quelque chose (prénom: "S", nom: "K")
  4. Le principal inconvénient est que vous n'avez pas accès à la référence 'this' dans Funcs et Actions. Par conséquent, toutes les dépendances de la classe doivent être explicitement transmises en tant que paramètres. C'est bon car vous connaissez toutes les dépendances, mais c'est dommage si vous avez plusieurs propriétés sur lesquelles votre Func dépendra. Votre kilométrage variera en fonction de votre cas d'utilisation.

Donc, pour résumer, utiliser Funcs et Actions for Unit est très utile 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 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!


-1

Utiliser Mock, c'est possible. Nuget: https://www.nuget.org/packages/moq/

Et croyez-moi, c'est assez simple et logique.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock a besoin de la méthode virtuelle à remplacer.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.