web-dev-qa-db-fra.com

Rendre publique une méthode privée pour le tester à l'unité… une bonne idée?

Note du modérateur: 39 réponses ont déjà été publiées ici (certaines ont été supprimées). Avant de publier votre réponse , déterminez si vous pouvez ou non ajouter quelque chose de significatif à la discussion. Vous ne faites probablement que répéter ce que quelqu'un d'autre a déjà dit.


Je me trouve parfois dans l'obligation de créer une méthode privée dans une classe public simplement pour écrire des tests unitaires.

Cela serait généralement dû au fait que la méthode contient une logique partagée par d'autres méthodes de la classe et qu'il est plus simple de tester la logique elle-même ou qu'une autre raison est possible: je veux tester la logique utilisée dans les threads synchrones sans avoir à se soucier des problèmes de thread. .

Est-ce que d'autres personnes se retrouvent dans cette situation, parce que je n'aime pas trop le faire? Personnellement, je pense que les bonus compensent les problèmes de rendre publique une méthode qui ne fournit pas vraiment de service en dehors de la classe ...

MISE À JOUR

Merci pour les réponses tout le monde, semble avoir piqué l'intérêt des gens. Je pense que le consensus général est que les tests devraient se faire via l’API publique car c’est le seul moyen d’utiliser une classe, et je suis d’accord avec cela. Les quelques cas que j'ai mentionnés ci-dessus et dans lesquels je ferais cela sont des cas rares et je pensais que les avantages de le faire en valaient la peine.

Je peux cependant voir que tout le monde souligne que cela ne devrait jamais se produire. Et quand j'y réfléchis un peu plus, je pense que changer votre code pour prendre en charge les tests est une mauvaise idée. Après tout, je suppose que les tests sont en quelque sorte un outil de support et changer de système pour "supporter un outil de support" si vous voulez est flagrant. mauvaise pratique.

282
jcvandan

Remarque:
Cette réponse a été initialement publiée pour la question Le test unitaire est-il déjà une bonne raison d’exposer des variables d’instance privées via des accesseurs? qui a été fusionné dans celui-ci, de sorte qu’il peut être un peu spécifique à la casse présentée ici.

En règle générale, je suis généralement favorable au refactoring du code "de production" afin de faciliter les tests. Cependant, je ne pense pas que ce serait un bon appel ici. Un bon test unitaire (généralement) ne devrait pas se soucier des détails d'implémentation de la classe, mais uniquement de son comportement visible. Au lieu d'exposer les piles internes au test, vous pouvez vérifier que la classe renvoie les pages dans l'ordre attendu après avoir appelé first() ou last().

Par exemple, considérons ce pseudo-code:

public class NavigationTest {
    private Navigation nav;

    @Before
    public void setUp() {
        // Set up nav so the order is page1->page2->page3 and
        // we've moved back to page2
        nav = ...;
    }

    @Test
    public void testFirst() {
        nav.first();

        assertEquals("page1", nav.getPage());

        nav.next();
        assertEquals("page2", nav.getPage());

        nav.next();
        assertEquals("page3", nav.getPage());
    }

    @Test
    public void testLast() {
        nav.last();

        assertEquals("page3", nav.getPage());

        nav.previous();
        assertEquals("page2", nav.getPage());

        nav.previous();
        assertEquals("page1", nav.getPage());
    }
}
182
Mureinik

Personnellement, je préférerais effectuer des tests unitaires à l’aide de l’API publique et je ne rendrais certainement jamais la méthode privée publique , mais seulement pour faciliter les tests.

Si vous voulez vraiment tester la méthode privée de manière isolée, vous pouvez utiliser Java) Easymock / Powermock pour vous permettre de le faire.

Vous devez être pragmatique à ce sujet et vous devez également être conscient des raisons pour lesquelles les choses sont difficiles à tester.

' Écoutez les tests ' - Si c'est difficile à tester, est-ce que cela vous dit quelque chose à propos de votre conception? Pourriez-vous préciser où un test pour cette méthode serait trivial et facilement couvert par un test via l'API publique?

Voici ce que Michael Feathers a à dire dans ' Travailler efficacement avec le code hérité "

"Beaucoup de gens passent beaucoup de temps à essayer de comprendre comment contourner ce problème ... la vraie réponse est que si vous avez envie de tester une méthode privée, la méthode ne doit pas être privée; si la méthode est publique, vous dérange, les chances sont, c'est parce que cela fait partie d'une responsabilité distincte, il devrait être sur une autre classe. " [ Travailler efficacement avec le code hérité (2005) de M. Feathers]

147
blank

Comme d'autres l'ont dit, il est quelque peu suspect d'essayer des méthodes privées. l'unité teste l'interface publique, pas les détails de l'implémentation privée.

Cela dit, la technique que j'utilise lorsque je veux tester quelque chose qui est privé en C # consiste à rétrograder la protection de l'accessibilité de privé à interne, puis à marquer l'assembly de test unitaire en tant qu'assemblage ami à l'aide de InternalsVisibleTo . L’ensemble de test unitaire sera alors autorisé à traiter les internes comme publics, mais vous n’aurez pas à craindre d’ajouter accidentellement à votre surface publique.

62
Eric Lippert

Beaucoup de réponses suggèrent de ne tester que l'interface publique, mais à mon humble avis, cela n'est pas réaliste: si une méthode effectue quelque chose en 5 étapes, vous voudrez peut-être tester ces cinq étapes séparément, pas tous ensemble. Cela nécessite de tester les cinq méthodes, ce qui (autre que pour le test) pourrait sinon être private.

La méthode habituelle de test des méthodes "privées" consiste à attribuer à chaque classe sa propre interface et à créer les méthodes "privées" public, sans toutefois les inclure dans l'interface. De cette façon, ils peuvent toujours être testés, mais ils ne gonflent pas l'interface.

Oui, cela entraînera un gonflement des fichiers et des classes.

Oui, cela rend les spécificateurs public et private redondants.

Oui, c'est une douleur dans le cul.

C’est malheureusement l’un des nombreux sacrifices que nous faisons pour rendre le code vérifiable. Peut-être qu'un futur langage (ou même une future version de C #/Java) aura-t-il des fonctionnalités facilitant la testabilité des classes et des modules; mais en attendant, nous devons sauter à travers ces cerceaux.


Certains diront que chacune de ces étapes devrait être sa propre classe , mais je ne suis pas d'accord - si elles partagent le même état, il n'y a aucune raison de créer cinq classes séparées où cinq méthodes feraient l'affaire. Pire encore, cela se traduit par un gonflement des fichiers et des classes. Plus, il infecte l'API publique de votre module - toutes ces classes doivent nécessairement être public si vous voulez les tester à partir d'un autre module (soit cela, soit inclure le code de test dans le même module, ce qui signifie que vous devez envoyer le code de test avec votre produit).

Un test unitaire devrait tester le contrat public, le seul moyen d'utiliser une classe dans d'autres parties du code. Une méthode privée concerne les détails de l'implémentation. Vous ne devez pas la tester. Dans la mesure où l'API publique fonctionne correctement, l'implémentation n'a pas d'importance et peut être modifiée sans modification dans les cas de test.

26
kan

OMI, vous devriez écrire vos tests sans faire de suppositions profondes sur la manière dont votre classe est implémentée à l’intérieur. Vous voudrez probablement le refactoriser plus tard en utilisant un autre modèle interne, tout en offrant les mêmes garanties que l'implémentation précédente.

Gardant cela à l'esprit, je vous suggère de vous concentrer sur la vérification de la validité de votre contrat, quelle que soit l'application interne de votre classe. Test basé sur les propriétés de vos API publiques.

20
SimY4

Que diriez-vous de rendre le paquet privé? Ensuite, votre code de test peut le voir (ainsi que d'autres classes de votre paquet), mais il est toujours masqué par vos utilisateurs.

Mais en réalité, vous ne devriez pas tester de méthodes privées. Ce sont des détails de mise en œuvre, et ne font pas partie du contrat. Tout ce qu'ils font devrait être couvert en appelant les méthodes publiques (s'ils ont un code qui n'est pas exercé par les méthodes publiques, alors cela devrait disparaître). Si le code privé est trop complexe, la classe fait probablement trop de choses et manque de refactoring.

Rendre une méthode publique est un grand engagement. Une fois que vous aurez fait cela, les gens pourront l’utiliser et vous ne pourrez plus les changer.

18
Thilo

Mise à jour : J'ai ajouté une réponse plus complète et plus complète à cette question dans de nombreux autres endroits. Vous pouvez le trouver sur mon blog. .

Si j’ai besoin de rendre public quelque chose pour le tester, cela indique généralement que le système testé ne suit pas le Principe de responsabilité unique . Il y a donc une classe manquante qui devrait être introduite. Après avoir extrait le code dans une nouvelle classe, rendez-le public. Vous pouvez maintenant tester facilement et vous suivez SRP. Votre autre classe doit simplement invoquer cette nouvelle classe via la composition.

Rendre les méthodes publiques/utiliser des astuces de langage telles que marquer le code comme visible pour tester les assemblées devrait toujours être un dernier recours.

Par exemple:

public class SystemUnderTest
{
   public void DoStuff()
   {
      // Blah
      // Call Validate()
   }

   private void Validate()
   {
      // Several lines of complex code...
   }
}

Refactoriser ceci en introduisant un objet validateur.

public class SystemUnderTest
{
    public void DoStuff()
    {
       // Blah
       validator.Invoke(..)
    }
}

Il ne nous reste plus qu'à vérifier que le validateur est appelé correctement. Le processus de validation actuel (la logique auparavant privée) peut être testé en isolation pure. Aucun test complexe ne sera nécessaire pour s'assurer que cette validation est réussie.

15
Finglas

Quelques bonnes réponses. Une chose que je n'ai pas vue mentionnée est qu'avec le développement piloté par les tests (TDD), des méthodes privées sont créées au cours de la phase de refactoring (regardez Extract Method pour un exemple de motif de refactoring), et devraient donc déjà la couverture de test nécessaire. Si cela est fait correctement (et bien sûr, vous obtiendrez une foule d'opinions lorsqu'il s'agira d'exactitude), vous ne devriez pas avoir à vous soucier de devoir rendre publique une méthode privée juste pour pouvoir la tester.

12
csano

Pourquoi ne pas scinder l'algorithme de gestion de pile en une classe d'utilitaires? La classe utilitaire peut gérer les piles et fournir des accesseurs publics. Ses tests unitaires peuvent être centrés sur les détails de la mise en œuvre. Les tests approfondis de classes à algorithmes complexes sont très utiles pour résoudre les cas Edge et assurer la couverture.

Ensuite, votre classe actuelle peut déléguer proprement à la classe d’utilitaire sans exposer les détails de l’implémentation. Ses tests porteront sur l'exigence de pagination comme l'ont recommandé d'autres.

10
Alfred Armstrong

En Java, il est également possible de le rendre paquet privé (c'est-à-dire en laissant le modificateur de visibilité). Si vos tests unitaires sont dans le même package que la classe en cours de test, il devrait alors être en mesure de voir ces méthodes, ce qui est un peu plus sûr que de rendre la méthode complètement publique.

9
Tom Jefferys

Les méthodes privées sont généralement utilisées comme méthodes "auxiliaires". Par conséquent, ils ne renvoient que des valeurs de base et n'agissent jamais sur des instances d'objets spécifiques.

Vous avez plusieurs options si vous voulez les tester.

  • Utiliser la réflexion
  • Donne accès au paquet de méthodes

Vous pouvez également créer une nouvelle classe avec la méthode d'assistance en tant que méthode publique si elle est un bon candidat pour une nouvelle classe.

Il y a un très bon article ici à ce sujet.

9
adamjmarkham

Utilisez la réflexion pour accéder aux variables privées si vous en avez besoin.

Mais en réalité, vous ne vous souciez pas de l'état interne de la classe, vous voulez simplement vérifier que les méthodes publiques retournent ce que vous attendez dans les situations que vous pouvez anticiper.

6
Daniel Alexiuc

Si vous utilisez C #, vous pouvez rendre la méthode interne. De cette façon, vous ne polluez pas les API publiques.

Ajoutez ensuite l'attribut à la dll

[Assembly: InternalsVisibleTo ("MyTestAssembly")]

Désormais, toutes les méthodes sont visibles dans votre projet MyTestAssembly. Peut-être pas parfait, mais mieux que de rendre publique la méthode privée juste pour le tester.

6
Piotr Perak

en termes de tests unitaires, vous ne devez absolument pas ajouter de méthodes supplémentaires; Je pense que vous feriez mieux de créer un scénario de test sur votre méthode first(), qui serait appelée avant chaque test; vous pouvez ensuite appeler plusieurs fois les - next(), previous() et last() pour voir si les résultats correspondent à vos attentes. Je suppose que si vous n’ajoutez pas plus de méthodes à votre classe (uniquement à des fins de test), vous vous en tiendriez au principe de la "boîte noire";

6
Ion

Premièrement, voyez si la méthode doit être extraite dans une autre classe et rendue publique. Si ce n'est pas le cas, rendez le package protégé et entrez Java annotez avec @ VisibleForTesting .

5
Noel Yap

Je dirais que c'est une mauvaise idée car je ne suis pas sûr que vous en retiriez des avantages ou des problèmes éventuels. Si vous modifiez le contrat d'un appel, juste pour tester une méthode privée, vous ne testez pas la classe de la façon dont elle serait utilisée, mais vous créez un scénario artificiel que vous n'aviez jamais prévu.

De plus, en déclarant la méthode publique, que dire dans six mois (après avoir oublié que la seule raison de rendre une méthode publique est de tester) que vous (ou si vous avez remis le projet) quelqu'un complètement différent a gagné ne l’utilisez pas, ce qui pourrait avoir des conséquences inattendues et/ou un cauchemar d’entretien.

5
beny23

Dans votre mise à jour, vous dites qu'il est bon de simplement tester à l'aide de l'API publique. Il y a en fait deux écoles ici.

  1. Test de la boîte noire

    L'école des boîtes noires dit que la classe devrait être considérée comme une boîte noire dans laquelle personne ne peut voir la mise en œuvre. La seule façon de tester cela consiste à utiliser l'API publique - tout comme l'utilisateur de la classe l'utilisera.

  2. test de la boîte blanche.

    L'école White Box pense qu'il est naturel d'utiliser les connaissances relatives à la mise en œuvre de la classe, puis de tester la classe pour savoir qu'elle fonctionne comme il se doit.

Je ne peux vraiment pas prendre parti dans la discussion. J'ai juste pensé qu'il serait intéressant de savoir qu'il existe deux manières distinctes de tester une classe (ou une bibliothèque ou autre).

5
bengtb

Les méthodes privées que vous souhaitez tester isolément indiquent qu'il existe un autre "concept" enfoui dans votre classe. Extrayez ce "concept" dans sa propre classe et testez-le comme une "unité" distincte.

Regardez cette vidéo pour une interprétation vraiment intéressante du sujet.

4
Jordão

Vous devez faire cela dans certaines situations (par exemple, lorsque vous implémentez des algorithmes complexes). Il suffit de le faire paquet-privé et cela suffira. Mais dans la plupart des cas, vous avez probablement des classes trop complexes, ce qui nécessite de factoriser la logique dans d'autres classes.

4

Vous ne devriez jamais laisser vos tests dicter votre code. Je ne parle pas de TDD ou d'autres DD Je veux dire, exactement ce que vous demandez. Votre application a-t-elle besoin de ces méthodes pour être publique? Si c'est le cas, testez-les. Si ce n'est pas le cas, ne les publiez pas pour les tester. Même chose avec les variables et les autres. Laissez les besoins de votre application dicter le code et laissez vos tests vérifier que le besoin est satisfait. (Encore une fois, je ne parle pas de tester d’abord ou pas, je parle de changer une structure de classes pour atteindre un objectif de test).

Au lieu de cela, vous devriez "tester plus haut". Testez la méthode qui appelle la méthode privée. Mais vos tests doivent tester les besoins de vos applications et non vos "décisions d'implémentation".

Par exemple (pseudo-code bod ici);

   public int books(int a) {
     return add(a, 2);
   }
   private int add(int a, int b) {
     return a+b;
   } 

Il n'y a aucune raison de tester "ajouter", vous pouvez tester des "livres" à la place.

Ne laissez jamais vos tests prendre les décisions de conception de code pour vous. Vérifiez que vous obtenez le résultat attendu, pas comment vous obtenez ce résultat.

3
coteyr

Guava dispose d'une annotation @VisibleForTesting pour les méthodes de marquage dont la portée (package ou public) est élargie. J'utilise une annotation @Private pour la même chose.

Bien que l’API publique doive être testée, il est parfois pratique et judicieux d’obtenir des contenus qui ne seraient normalement pas publics.

Quand:

  • une classe est rendue nettement moins lisible, en tout, en la divisant en plusieurs classes,
  • juste pour le rendre plus testable,
  • et en fournissant un accès test aux entrailles ferait l'affaire

il semble que la religion l'emporte sur l'ingénierie.

2
Ed Staub

Je laisse généralement ces méthodes sous la forme protected et place le test unitaire dans le même package (mais dans un autre dossier de projet ou source), où elles peuvent accéder à toutes les méthodes protégées car le chargeur de classes les placera dans le même espace de noms. .

2
hiergiltdiestfu

Je suis plutôt d’accord pour dire que les avantages que présente cette unité testée compensent les problèmes posés par l’augmentation de la visibilité de certains membres. Une légère amélioration consiste à le rendre protégé et virtuel, puis à le remplacer dans une classe de test pour l'exposer.

Sinon, si c'est une fonctionnalité que vous souhaitez tester séparément, cela ne suggère-t-il pas un objet manquant dans votre conception? Peut-être que vous pourriez le mettre dans une classe distincte testable ... alors votre classe existante ne fait que déléguer à une instance de cette nouvelle classe.

2
IanR

Non, car il existe de meilleurs moyens de dépouiller ce chat.

Certains faisceaux de tests unitaires reposent sur des macros dans la définition de classe, qui se développent automatiquement pour créer des points d'ancrage lors de la création en mode test. Très style C, mais ça marche.

Un moyen plus simple OO est de rendre tout ce que vous voulez tester "protégé" plutôt que "privé". Le faisceau de test hérite de la classe sous test et peut ensuite accéder à tous les membres protégés.

Ou vous optez pour l'option "ami". Personnellement, c’est la fonctionnalité de C++ que j’aime le moins, car elle enfreint les règles d’encapsulation, mais elle s’avère nécessaire à la façon dont le C++ implémente certaines fonctionnalités, alors hé ho.

Quoi qu’il en soit, si vous effectuez des tests unitaires, vous aurez tout autant besoin d’injecter des valeurs à ces membres. Envoyer des SMS est parfaitement valide. Et cela vraiment serait briser votre encapsulation.

2
Graham

J'ajouterai souvent une méthode appelée quelque chose comme validate, verify, check, etc., à une classe afin qu'elle puisse être appelée pour tester l'état interne d'un objet.

Parfois, cette méthode est encapsulée dans un bloc ifdef (j'écris principalement en C++), de sorte qu'elle n'est pas compilée pour la publication. Mais il est souvent utile en version de fournir des méthodes de validation qui guident l'arborescence des objets du programme pour vérifier les choses.

2
Zan Lynx

Je conserve généralement les classes de test dans le même projet/assemblage que les classes sous test.
De cette façon, je n'ai besoin que de internal visibilité pour rendre les fonctions/classes testables.

Cela complique quelque peu votre processus de construction, qui doit filtrer les classes de test. J'y parviens en nommant toutes mes classes de test TestedClassTest et en utilisant une expression régulière pour filtrer ces classes.

Ceci ne concerne bien sûr que la partie C #/.NET de votre question

2
yas4891

Comme le soulignent largement les commentaires d'autres personnes, les tests unitaires doivent être axés sur l'API publique. Cependant, mis à part le pour et le contre et la justification, vous pouvez appeler des méthodes privées dans un test unitaire en utilisant la réflexion. Vous devez bien sûr vous assurer que votre sécurité JRE le permet. L'appel de méthodes privées est quelque chose que Spring Framework utilise avec son ReflectionUtils (voir la méthode makeAccessible(Method)).

Voici un petit exemple de classe avec une méthode d'instance privée.

public class A {
    private void doSomething() {
        System.out.println("Doing something private.");
    }
}

Et un exemple de classe qui exécute la méthode d'instance privée.

import Java.lang.reflect.InvocationTargetException;
import Java.lang.reflect.Method;
public class B {
    public static final void main(final String[] args) {
        try {
            Method doSomething = A.class.getDeclaredMethod("doSomething");
            A o = new A();
            //o.doSomething(); // Compile-time error!
            doSomething.setAccessible(true); // If this is not done, you get an IllegalAccessException!
            doSomething.invoke(o);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }
}

L'exécution de B imprimera Doing something private. Si vous en avez vraiment besoin, la réflexion peut être utilisée dans les tests unitaires pour accéder aux méthodes d'instance privée.

1
Dan Cruz

En .Net, il existe une classe spéciale appelée PrivateObject conçue spécifiquement pour vous permettre d'accéder aux méthodes privées d'une classe.

Voir plus à ce sujet sur le MSDN ou ici sur débordement de pile

(Je me demande si personne n'en a parlé jusqu'à présent.)

Il y a des situations où cela ne suffit pas, auquel cas vous devrez utiliser la réflexion.

J'adhérerais toujours à la recommandation générale de ne pas tester les méthodes privées, cependant, comme d'habitude, il y a toujours des exceptions.

1
yoel halb

Très réponse à la question.
IHMO, l'excellent réponse de @BlueRaja - Danny Pflughoeft est l'un des meilleurs.

Beaucoup de réponses suggèrent de ne tester que l'interface publique, mais à mon humble avis, cela n'est pas réaliste: si une méthode effectue quelque chose en 5 étapes, vous voudrez peut-être tester ces cinq étapes séparément, pas tous ensemble. Cela nécessite de tester les cinq méthodes qui, autrement que pour les tests, pourraient être privées.


Avant tout, je tiens à souligner que la question "Devrions-nous rendre publique une méthode privée pour le tester à l'unité" est une question à laquelle une réponse objectivement correcte dépend de plusieurs paramètres.
Je pense donc que dans certains cas nous n'avons pas à et dans d'autres nous devrions.

Voici quelques réponses qui pourraient être résumées comme suit: "C'est souvent bien de faire ça" ou "jamais, c'est mauvais. Ne trichez pas avec l'API et testez uniquement le public comportement ".
Cela me contrarie beaucoup car la qualité de la conception pour les tests et la mise en œuvre est une question importante et cette question implique de nombreuses conséquences pour les deux.


Rendre publique une méthode privée ou extraire la méthode privée en tant que méthode publique dans une autre classe (nouvelle ou existante)?

Dans les cas où il est acceptable d'élargir la visibilité de la méthode private, la solution reposant sur la méthode private ainsi que dans la méthode simulée public n'est souvent pas la meilleure solution.
Cela diminue la qualité de la conception et la testabilité de la classe.
Un test unitaire doit tester le comportement de un Méthode/fonction API.
Si vous testez une méthode public qui appelle une autre méthode public appartenant au même composant, vous n'avez pas test de l'unité la méthode. Vous testez plusieurspublic méthodes en même temps.
En conséquence, vous pouvez dupliquer des tests, des montages de test, des assertions de test, la maintenance du test et plus généralement la conception de l'application.
À mesure que la valeur des tests diminue, ils perdent souvent tout intérêt pour les développeurs qui les écrivent ou les maintiennent.

Pour éviter toute cette duplication, au lieu de créer la méthode private, public, une solution préférable est généralement extraire la méthode private en tant que public méthode dans une classe nouvelle ou existante.
Il ne créera pas de défaut de conception.
Cela rendra le code plus significatif et la classe moins lourde.
En outre, la méthode private est parfois une routine/sous-ensemble de la classe alors que le comportement convient mieux à une structure spécifique.
Enfin, cela rend également le code plus vérifiable et évite la duplication des tests.
Nous pouvons en effet empêcher la duplication de tests en testant à l'unité la méthode public dans sa propre classe de test et dans la classe de test des classes clientes, il suffit de simuler la dépendance.

Se moquer des méthodes privées?

Bien sûr, cela est possible en utilisant la réflexion ou avec des outils comme PowerMock, mais IHMO est, à mon avis, l’un des moyens de contourner un problème de conception.
Une classe de test est une classe comme une autre.
Un membre private n'est pas conçu pour être exposé à d'autres classes. Nous devrions donc suivre la même règle pour les classes de test.

Se moquer des méthodes publiques de l'objet testé?

Vous voudrez peut-être changer le modificateur private en public pour tester la méthode.
Ensuite, pour tester la méthode qui utilise cette méthode publique refactorisée, vous pouvez être tenté de se moquer de la méthode refactorisée public en utilisant des outils tels que Mockito ( concept d'espionnage ) mais de manière similaire. pour se moquer de private méthodes, il faut éviter de se moquer de l'objet à tester.

La documentation Mockito.spy() le dit elle-même:

Crée un espion de l'objet réel. L'espion appelle de vraies méthodes, sauf si>> elles sont écrasées.

Les vrais espions doivent être utilisés avec précaution et occasionnellement, par exemple pour traiter du code hérité.

Par expérience, utiliser spy() diminue généralement la qualité du test et sa lisibilité.
En outre, il est beaucoup plus sujet aux erreurs car l’objet à tester est à la fois un objet fictif et un objet réel.
C’est souvent la meilleure façon d’écrire un test d’acceptation non valide.


Voici un guide que j'utilise pour décider si une méthode private doit rester private ou être refactorisée.

Cas 1) Ne créez jamais une méthode privatepublic si cette méthode est invoquée une fois.
C'est une méthode private pour un seul. Vous ne pouvez donc jamais dupliquer la logique de test telle qu'elle est invoquée une fois.

Cas 2) Vous devriez vous demander si une méthode private doit être refactorisée en tant que méthode public si la méthode private est invoquée plusieurs fois .

Comment décider?

  • La méthode private ne produit pas de duplication dans les tests.
    -> Conservez la méthode private telle quelle.

  • La méthode private produit une duplication dans les tests. Autrement dit, vous devez répéter certains tests pour affirmer la même logique pour chaque test que les méthodes unit-test public utilisent la méthode private.
    -> Si le traitement répété peut faire partie de l'API fournie aux clients (pas de problème de sécurité, pas de traitement interne, etc.), extraire le private comme une méthode public dans une nouvelle classe.
    -> Sinon, si le traitement répété ne doit pas faire partie de l'API fournie aux clients (problème de sécurité, traitement interne, etc.), n'élargissez pas la visibilité de la méthode private à public.
    Vous pouvez la laisser inchangée ou déplacer la méthode dans une classe de paquetages private qui ne fera jamais partie de l'API et qui ne serait jamais accessible par les clients.


Exemples de code

Les exemples s'appuient sur Java et les bibliothèques suivantes: JUnit, AssertJ (assertion matcher) et Mockito.
Mais je pense que l'approche globale est également valable pour C #.

1) Exemple où la méthode private ne crée pas de duplication dans le code de test

Voici une classe Computation qui fournit des méthodes pour effectuer certains calculs.
Toutes les méthodes publiques utilisent la méthode mapToInts().

public class Computation {

    public int add(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] + ints[1];
    }

    public int minus(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] - ints[1];
    }

    public int multiply(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] * ints[1];
    }

    private int[] mapToInts(String a, String b) {
        return new int[] { Integer.parseInt(a), Integer.parseInt(b) };
    }

}

Voici le code de test:

public class ComputationTest {

    private Computation computation = new Computation();

    @Test
    public void add() throws Exception {
        Assert.assertEquals(7, computation.add("3", "4"));
    }

    @Test
    public void minus() throws Exception {
        Assert.assertEquals(2, computation.minus("5", "3"));
    }

    @Test
    public void multiply() throws Exception {
        Assert.assertEquals(100, computation.multiply("20", "5"));
    }

}

Nous avons pu constater que l’appel de la méthode private _ mapToInts() ne duplique pas la logique de test.
Il s’agit d’une opération intermédiaire qui ne produit pas un résultat spécifique que nous devons affirmer lors des tests.

2) Exemple dans lequel la méthode private crée une duplication indésirable dans le code de test

Voici une classe MessageService qui fournit des méthodes pour créer des messages.
Toutes les méthodes public utilisent la méthode createHeader():

public class MessageService {

    public Message createMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

    private Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

Voici le code de test:

import Java.time.LocalDate;

import org.assertj.core.api.Assertions;
import org.junit.Test;

import junit.framework.Assert;

public class MessageServiceTest {

    private MessageService messageService = new MessageService();

    @Test
    public void createMessage() throws Exception {
        final String inputMessage = "simple message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), false);
    }

    @Test
    public void createEncryptedMessage() throws Exception {
        final String inputMessage = "encryted message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createEncryptedMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals("Aç4B36ddflm1Dkok49d1d9gaz", actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), true);
    }

    @Test
    public void createAnonymousMessage() throws Exception {
        final String inputMessage = "anonymous message";
        Message actualMessage = messageService.createAnonymousMessage(inputMessage);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);
    }

}

Nous avons constaté que l’appel de la méthode private méthode createHeader() crée une duplication dans la logique de test.
createHeader() crée en effet un résultat spécifique que nous devons affirmer dans les tests.
Nous affirmons 3 fois le contenu de l'en-tête alors qu'une seule assertion devrait être requise.

Nous pourrions également noter que la duplication d’affirmation est étroite entre les méthodes mais pas nécessairement la même chose car la méthode private a une logique spécifique: nous pourrions bien entendu avoir plus de différences en fonction de la complexité logique de la private méthode.
De plus, à chaque fois que nous ajoutons une nouvelle méthode public dans MessageService qui appelle createHeader(), nous devrons ajouter cette assertion.
Notez également que si createHeader() modifie son comportement, tous ces tests devront peut-être également être modifiés.
En définitive, ce n’est pas un très bon design.

Étape du refactoring

Supposons que nous sommes dans un cas où createHeader() est acceptable pour faire partie de l'API.
Nous allons commencer par refactoriser la classe MessageService en remplaçant le modificateur d'accès de createHeader() par public:

public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
    return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
}

Nous pourrions maintenant tester cette méthode unitaire:

@Test
public void createHeader_with_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = true;
  // action
  Header actualHeader = messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), true);
}

@Test
public void createHeader_with_not_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = false;
  // action
  messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);

}

Mais qu'en est-il des tests que nous écrivons précédemment pour public méthodes de la classe qui utilisent createHeader()?
Pas beaucoup de différences.
En fait, nous sommes toujours ennuyés car ces méthodes public doivent encore être testées concernant la valeur de l’en-tête renvoyé.
Si nous supprimons ces assertions, nous ne pourrons peut-être pas détecter de régression à ce sujet.
Nous devrions pouvoir naturellement isoler ce traitement, mais nous ne pouvons pas, car la méthode createHeader() appartient au composant testé.
C'est pourquoi j'ai expliqué au début de ma réponse que dans la plupart des cas, nous devrions privilégier l'extraction de la méthode private d'une autre classe au changement du modificateur d'accès en public.

Nous introduisons donc HeaderService:

public class HeaderService {

    public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

Et nous migrons les tests createHeader() dans HeaderServiceTest.

Maintenant, MessageService est défini avec une dépendance HeaderService:

public class MessageService {

    private HeaderService headerService;

    public MessageService(HeaderService headerService) {
        this.headerService = headerService;
    }

    public Message createMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = headerService.createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

}

Et dans les tests MessageService, nous n'avons plus besoin d'affirmer chaque valeur d'en-tête, celle-ci étant déjà testée.
Nous voulons simplement nous assurer que Message.getHeader() renvoie ce que HeaderService.createHeader() est retourné.

Par exemple, voici la nouvelle version de createMessage() test:

@Test
public void createMessage() throws Exception {
    final String inputMessage = "simple message";
    final Credentials inputCredentials = new Credentials("user", "pass");
    final Header fakeHeaderForMock = createFakeHeader();
    Mockito.when(headerService.createHeader(inputCredentials, inputMessage, false))
           .thenReturn(fakeHeaderForMock);
    // action
    Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
    // assertion
    Assert.assertEquals(inputMessage, actualMessage.getMessage());
    Assert.assertSame(fakeHeaderForMock, actualMessage.getHeader());
}

Notez que assertSame() permet de comparer les références d'objet pour les en-têtes et non le contenu.
Maintenant, HeaderService.createHeader() peut changer de comportement et renvoyer des valeurs différentes, cela n'a pas d'importance du point de vue des tests MessageService.

0
davidxxx

Personnellement, je rencontre les mêmes problèmes lorsque je teste des méthodes privées, car certains outils de test sont limités. Il n'est pas bon que votre conception soit pilotée par des outils limités si elles ne répondent pas à votre besoin, changez l'outil et non la conception. Parce que votre demande pour C #, je ne peux pas proposer de bons outils de test, mais pour Java, il existe deux outils puissants: TestNG et PowerMock, et vous pouvez trouver les outils de test correspondants pour la plate-forme .NET

0
Xoke

Le but de l’unité de test est de confirmer le fonctionnement de l’API publique pour cette unité. Il ne devrait pas être nécessaire de créer une méthode privée uniquement à des fins de test. Dans ce cas, votre interface doit être repensée. Les méthodes privées peuvent être considérées comme des méthodes "auxiliaires" pour l'interface publique et sont donc testées via l'interface publique car elles feraient appel aux méthodes privées.

La seule raison pour laquelle je peux voir que vous avez un "besoin" de le faire est que votre classe n'est pas correctement conçue pour les tests.

0
Chris Johnson

Tout est question de pragmatisme. Vos tests unitaires sont un client du code en quelque sorte et pour atteindre un bon niveau de couverture de code, vous devez rendre votre code très testable. Votre solution sera un échec potentiel si le code de test est extrêmement complexe afin de vous permettre de configurer les casiers nécessaires sans que le code ne soit réellement verrouillé par le public. Utiliser IoC aide aussi dans cette affaire.

0
Dean Chalk