Comment tester une fonction privée ou une classe possédant des méthodes, des champs ou des classes internes privées ?

StackOverflow https://stackoverflow.com/questions/34571

  •  09-06-2019
  •  | 
  •  

Question

Comment tester unitairement (à l'aide de xUnit) une classe qui possède des méthodes privées internes, des champs ou des classes imbriquées ?Ou une fonction rendue privée en ayant lien interne (static en C/C++) ou est dans un privé (anonyme) espace de noms ?

Il semble mauvais de modifier le modificateur d'accès d'une méthode ou d'une fonction simplement pour pouvoir exécuter un test.

Était-ce utile?

La solution

Mise à jour:

Environ 10 ans plus tard, la meilleure façon de tester une méthode privée ou tout membre inaccessible est peut-être via @Jailbreak du Collecteur cadre.

@Jailbreak Foo foo = new Foo();
// Direct, *type-safe* access to *all* foo's members
foo.privateMethod(x, y, z);
foo.privateField = value;

De cette façon, votre code reste sécurisé et lisible.Aucun compromis de conception, pas de méthodes et de champs surexposés au profit des tests.

Si vous avez un héritage Java application, et vous n'êtes pas autorisé à modifier la visibilité de vos méthodes, la meilleure façon de tester les méthodes privées est d'utiliser réflexion.

En interne, nous utilisons des assistants pour obtenir/définir private et private static variables ainsi que d'invoquer private et private static méthodes.Les modèles suivants vous permettront de faire à peu près tout ce qui concerne les méthodes et champs privés.Bien sûr, tu ne peux pas changer private static final variables par réflexion.

Method method = TargetClass.getDeclaredMethod(methodName, argClasses);
method.setAccessible(true);
return method.invoke(targetObject, argObjects);

Et pour les champs :

Field field = TargetClass.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);

Remarques:
1. TargetClass.getDeclaredMethod(methodName, argClasses) vous permet d'examiner private méthodes.La même chose s'applique pour getDeclaredField.
2.Le setAccessible(true) est obligé de jouer avec les soldats.

Autres conseils

La meilleure façon de tester une méthode privée consiste à utiliser une autre méthode publique.Si cela ne peut pas être fait, alors l’une des conditions suivantes est remplie :

  1. La méthode privée est du code mort
  2. Il y a une odeur de design à proximité de la classe que vous testez
  3. La méthode que vous essayez de tester ne doit pas être privée

Lorsque j'ai des méthodes privées dans une classe qui sont suffisamment compliquées pour que je ressens le besoin de tester directement les méthodes privées, c'est une odeur de code :mon cours est trop compliqué.

Mon approche habituelle pour résoudre de tels problèmes consiste à créer une nouvelle classe contenant les éléments intéressants.Souvent, cette méthode et les champs avec lesquels elle interagit, et peut-être une ou deux autres méthodes, peuvent être extraites dans une nouvelle classe.

La nouvelle classe expose ces méthodes comme « publiques », elles sont donc accessibles pour les tests unitaires.Les nouvelles et anciennes classes sont désormais plus simples que la classe originale, ce qui est très bien pour moi (je dois garder les choses simples, sinon je me perds !).

Notez que je ne suggère pas aux gens de créer des cours sans utiliser leur cerveau !Le but ici est d'utiliser les forces des tests unitaires pour vous aider à trouver de bonnes nouvelles classes.

j'ai utilisé réflexion faire cela pour Java dans le passé, et à mon avis, c'était une grosse erreur.

À proprement parler, vous devriez pas écrire des tests unitaires qui testent directement les méthodes privées.Qu'est-ce que tu devrait be testing est le contrat public que la classe a avec d'autres objets ;vous ne devriez jamais tester directement les composants internes d'un objet.Si un autre développeur souhaite apporter une petite modification interne à la classe, qui n'affecte pas le contrat public de la classe, il doit alors modifier votre test basé sur la réflexion pour s'assurer qu'il fonctionne.Si vous faites cela à plusieurs reprises tout au long d'un projet, les tests unitaires cessent alors d'être une mesure utile de la santé du code et commencent à devenir un obstacle au développement et une gêne pour l'équipe de développement.

Ce que je recommande à la place, c'est d'utiliser un outil de couverture de code tel que Cobertura, pour garantir que les tests unitaires que vous écrivez fournissent une couverture décente du code dans les méthodes privées.De cette façon, vous testez indirectement ce que font les méthodes privées et maintenez un niveau d’agilité plus élevé.

Extrait de cet article : Test des méthodes privées avec JUnit et SuiteRunner (Bill Venners), vous avez essentiellement 4 options :

  • Ne testez pas les méthodes privées.
  • Donnez l’accès au package de méthodes.
  • Utilisez une classe de test imbriquée.
  • Utilisez la réflexion.

Généralement, un test unitaire est destiné à exercer l'interface publique d'une classe ou d'une unité.Par conséquent, les méthodes privées constituent des détails d’implémentation que vous ne vous attendez pas à tester explicitement.

Juste deux exemples où je voudrais tester une méthode privée :

  1. Routines de décryptage - Je ne voudrais pas les rendre visibles pour quiconque pour voir juste pour les tests, sinon n'importe qui peut les utiliser pour décrypter.Mais ils sont intrinsèques au code, compliqués et doivent toujours fonctionner (l'exception évidente est la réflexion qui peut être utilisée pour visualiser même des méthodes privées dans la plupart des cas, lorsque SecurityManager n'est pas configuré pour empêcher cela).
  2. Création d'un SDK pour la consommation communautaire.Ici, le public prend un sens entièrement différent, car c'est le code que le monde entier peut voir (pas seulement interne à ma demande).Je mets du code dans des méthodes privées si je ne veux pas que les utilisateurs du SDK le voient - je ne vois pas cela comme une odeur de code, simplement le fonctionnement de la programmation SDK.Mais bien sûr, j'ai encore besoin de tester mes méthodes privées, et c'est là que la fonctionnalité de mon SDK vit réellement.

Je comprends l'idée de tester uniquement le "contrat".Mais je ne vois pas que l'on puisse préconiser de ne pas tester le code - votre kilométrage peut varier.

Mon compromis consiste donc à compliquer les JUnits avec la réflexion, plutôt que de compromettre ma sécurité et mon SDK.

Les méthodes privées sont appelées par une méthode publique, donc les entrées de vos méthodes publiques doivent également tester les méthodes privées appelées par ces méthodes publiques.Lorsqu’une méthode publique échoue, cela peut être un échec de la méthode privée.

Une autre approche que j'ai utilisée consiste à modifier une méthode privée en package private ou protected, puis à la compléter avec le @VisibleForTesting annotation de la bibliothèque Google Guava.

Cela indiquera à toute personne utilisant cette méthode de faire preuve de prudence et de ne pas y accéder directement, même dans un package.De plus, une classe de test n'a pas besoin d'être dans le même package physiquement, mais dans le même emballage sous le test dossier.

Par exemple, si une méthode à tester est en src/main/java/mypackage/MyClass.java alors votre appel test doit être passé dans src/test/java/mypackage/MyClassTest.java.De cette façon, vous avez accès à la méthode de test dans votre classe de test.

Pour tester du code existant avec des classes volumineuses et originales, il est souvent très utile de pouvoir tester la méthode privée (ou publique) que j'écris. tout de suite.

Je utilise l junitx.util.PrivateAccessor-package pour Java .De nombreuses phrases utiles pour accéder aux méthodes privées et aux champs privés.

import junitx.util.PrivateAccessor;

PrivateAccessor.setField(myObjectReference, "myCrucialButHardToReachPrivateField", myNewValue);
PrivateAccessor.invoke(myObjectReference, "privateMethodName", java.lang.Class[] parameterTypes, java.lang.Object[] args);

Après avoir essayé celui de Cem Catikkas solution utilisant la réflexion pour Java, je dois dire que c'était une solution plus élégante que celle que j'ai décrite ici.Cependant, si vous recherchez une alternative à l'utilisation de la réflexion et que vous avez accès à la source que vous testez, cela reste une option.

Il peut être utile de tester les méthodes privées d'une classe, en particulier avec développement piloté par les tests, où vous souhaitez concevoir de petits tests avant d'écrire du code.

La création d'un test avec accès aux membres et méthodes privés peut tester des zones de code difficiles à cibler spécifiquement avec un accès uniquement aux méthodes publiques.Si une méthode publique comporte plusieurs étapes, elle peut être constituée de plusieurs méthodes privées, qui peuvent ensuite être testées individuellement.

Avantages :

  • Peut tester une granularité plus fine

Désavantages:

  • Le code de test doit résider dans le même fichier que le code source, qui peut être plus difficile à maintenir
  • De même avec les fichiers de sortie .class, ils doivent rester dans le même package comme déclaré dans le code source

Cependant, si des tests continus nécessitent cette méthode, cela peut indiquer que les méthodes privées doivent être extraites, qui pourraient être testées de la manière publique traditionnelle.

Voici un exemple alambiqué de la façon dont cela fonctionnerait :

// Import statements and package declarations

public class ClassToTest
{
    private int decrement(int toDecrement) {
        toDecrement--;
        return toDecrement;
    }

    // Constructor and the rest of the class

    public static class StaticInnerTest extends TestCase
    {
        public StaticInnerTest(){
            super();
        }

        public void testDecrement(){
            int number = 10;
            ClassToTest toTest= new ClassToTest();
            int decremented = toTest.decrement(number);
            assertEquals(9, decremented);
        }

        public static void main(String[] args) {
            junit.textui.TestRunner.run(StaticInnerTest.class);
        }
    }
}

La classe interne serait compilée pour ClassToTest$StaticInnerTest.

Voir également: Astuce Java 106 :Des classes internes statiques pour le plaisir et le profit

Comme d'autres l'ont dit...ne testez pas directement les méthodes privées.Voici quelques réflexions :

  1. Gardez toutes les méthodes petites et ciblées (faciles à tester, faciles à trouver ce qui ne va pas)
  2. Utilisez des outils de couverture de code.J'aime Cobertura (oh bonne journée, on dirait qu'une nouvelle version est sortie !)

Exécutez la couverture du code sur les tests unitaires.Si vous constatez que les méthodes ne sont pas entièrement testées, complétez les tests pour augmenter la couverture.Visez une couverture de code à 100 %, mais sachez que vous ne l'obtiendrez probablement pas.

Les méthodes privées sont consommées par les méthodes publiques.Sinon, c'est du code mort.C'est pourquoi vous testez la méthode publique, en affirmant les résultats attendus de la méthode publique et donc les méthodes privées qu'elle consomme.

Les tests des méthodes privées doivent être testés par débogage avant d'exécuter vos tests unitaires sur les méthodes publiques.

Ils peuvent également être débogués à l'aide du développement piloté par les tests, déboguant vos tests unitaires jusqu'à ce que toutes vos assertions soient satisfaites.

Personnellement, je pense qu'il est préférable de créer des classes en utilisant TDD ;créer les stubs de méthode publique, puis générer des tests unitaires avec tous les assertions définies à l'avance, de sorte que le résultat attendu de la méthode est déterminé avant de la coder.De cette façon, vous ne vous tromperez pas en faisant en sorte que les assertions des tests unitaires correspondent aux résultats.Votre classe est alors robuste et répond aux exigences lorsque tous vos tests unitaires réussissent.

Dans le Cadre de printemps vous pouvez tester des méthodes privées en utilisant cette méthode :

ReflectionTestUtils.invokeMethod()

Par exemple:

ReflectionTestUtils.invokeMethod(TestClazz, "createTest", "input data");

Si vous utilisez Spring, RéflexionTestUtils fournit des outils pratiques qui vous aident ici avec un minimum d'effort.Par exemple, pour mettre en place un mock sur un membre privé sans être obligé d'ajouter un setter public indésirable :

ReflectionTestUtils.setField(theClass, "theUnsettableField", theMockObject);

Si vous essayez de tester du code existant que vous êtes réticent ou incapable de modifier, la réflexion est un bon choix.

Si la conception de la classe est toujours flexible et que vous disposez d'une méthode privée complexe que vous souhaitez tester séparément, je vous suggère de la retirer dans une classe distincte et de tester cette classe séparément.Cela ne doit pas nécessairement changer l'interface publique de la classe d'origine ;il peut créer en interne une instance de la classe d'assistance et appeler la méthode d'assistance.

Si vous souhaitez tester des conditions d'erreur difficiles provenant de la méthode d'assistance, vous pouvez aller plus loin.Extrayez une interface de la classe d'assistance, ajoutez un getter et un setter publics à la classe d'origine pour injecter la classe d'assistance (utilisée via son interface), puis injectez une version fictive de la classe d'assistance dans la classe d'origine pour tester comment la classe d'origine répond aux exceptions de l'assistant.Cette approche est également utile si vous souhaitez tester la classe d'origine sans tester également la classe auxiliaire.

Tester les méthodes privées brise l'encapsulation de votre classe car chaque fois que vous modifiez l'implémentation interne, vous cassez le code client (dans ce cas, les tests).

Ne testez donc pas les méthodes privées.

La réponse de Page FAQ de JUnit.org:

Mais s'il le faut...

Si vous utilisez JDK 1.3 ou supérieur, vous pouvez utiliser la réflexion pour subvertir le mécanisme de contrôle d'accès à l'aide du PrivilegedAccessor.Pour plus de détails sur la façon de l'utiliser, lisez Cet article.

Si vous utilisez JDK 1.6 ou supérieur et que vous annotez vos tests avec @Test, vous pouvez utiliser Dp4j pour injecter de la réflexion dans vos méthodes de tests.Pour plus de détails sur la façon de l'utiliser, voir ce script de test.

P.S.Je suis le principal contributeur de Dp4j, demander moi si tu as besoin d'aide.:)

Si vous souhaitez tester les méthodes privées d'une application héritée dont vous ne pouvez pas modifier le code, une option pour Java est jMockit, ce qui vous permettra de créer des simulations d'un objet même lorsqu'il est privé de la classe.

J'ai tendance à ne pas tester les méthodes privées.Là est la folie.Personnellement, je pense que vous ne devriez tester que vos interfaces exposées publiquement (et cela inclut les méthodes protégées et internes).

Si vous utilisez JUnit, jetez un œil à junit-addons.Il a la capacité d'ignorer le modèle de sécurité Java et d'accéder aux méthodes et attributs privés.

Une méthode privée n’est accessible qu’au sein de la même classe.Il n'y a donc aucun moyen de tester une méthode « privée » d'une classe cible à partir d'une classe de test.Une solution est que vous pouvez effectuer des tests unitaires manuellement ou changer votre méthode de « privée » à « protégée ».

Et puis, une méthode protégée n’est accessible que dans le même package où la classe est définie.Ainsi, tester une méthode protégée d’une classe cible signifie que nous devons définir votre classe de test dans le même package que la classe cible.

Si tout ce qui précède ne répond pas à vos besoins, utilisez la voie de la réflexion pour accéder à la méthode privée.

Je vous suggère de refactoriser un peu votre code.Lorsque vous devez commencer à penser à utiliser la réflexion ou d'autres types de choses, simplement pour tester votre code, quelque chose ne va pas avec votre code.

Vous avez évoqué différents types de problèmes.Commençons par les champs privés.Dans le cas de champs privés, j'aurais ajouté un nouveau constructeur et y aurais injecté des champs.Au lieu de cela:

public class ClassToTest {

    private final String first = "first";
    private final List<String> second = new ArrayList<>();
    ...
}

J'aurais utilisé ceci :

public class ClassToTest {

    private final String first;
    private final List<String> second;

    public ClassToTest() {
        this("first", new ArrayList<>());
    }

    public ClassToTest(final String first, final List<String> second) {
        this.first = first;
        this.second = second;
    }
    ...
}

Ce ne sera pas un problème même avec du code existant.L'ancien code utilisera un constructeur vide, et si vous me le demandez, le code refactorisé aura l'air plus propre et vous pourrez injecter les valeurs nécessaires dans le test sans réflexion.

Parlons maintenant des méthodes privées.D'après mon expérience personnelle, lorsque vous devez supprimer une méthode privée pour tester, cette méthode n'a rien à voir dans cette classe.Un modèle courant, dans ce cas, serait de envelopper dans une interface, comme Callable puis vous transmettez également cette interface dans le constructeur (avec cette astuce de constructeur multiple) :

public ClassToTest() {
    this(...);
}

public ClassToTest(final Callable<T> privateMethodLogic) {
    this.privateMethodLogic = privateMethodLogic;
}

La plupart du temps, tout ce que j'ai écrit ressemble à un modèle d'injection de dépendances.D'après mon expérience personnelle, c'est vraiment utile lors des tests, et je pense que ce type de code est plus propre et plus facile à maintenir.Je dirais la même chose à propos des classes imbriquées.Si une classe imbriquée contient une logique lourde, il serait préférable que vous la déplaciez en tant que classe privée de package et que vous l'injectiez dans une classe qui en a besoin.

Il existe également plusieurs autres modèles de conception que j'ai utilisés lors de la refactorisation et de la maintenance du code existant, mais tout dépend des cas de votre code à tester.Utiliser la réflexion n'est généralement pas un problème, mais lorsque vous avez une application d'entreprise qui est fortement testée et que des tests sont exécutés avant chaque déploiement, tout devient vraiment lent (c'est juste ennuyeux et je n'aime pas ce genre de choses).

Il existe également une injection de setter, mais je ne recommanderais pas de l'utiliser.Je ferais mieux de m'en tenir à un constructeur et de tout initialiser quand c'est vraiment nécessaire, en laissant la possibilité d'injecter les dépendances nécessaires.

Comme beaucoup l'ont suggéré ci-dessus, un bon moyen consiste à les tester via vos interfaces publiques.

Si vous faites cela, c'est une bonne idée d'utiliser un outil de couverture de code (comme Emma) pour voir si vos méthodes privées sont effectivement exécutées à partir de vos tests.

Voici ma fonction générique pour tester les champs privés :

protected <F> F getPrivateField(String fieldName, Object obj)
    throws NoSuchFieldException, IllegalAccessException {
    Field field =
        obj.getClass().getDeclaredField(fieldName);

    field.setAccessible(true);
    return (F)field.get(obj);
}

Veuillez voir ci-dessous pour un exemple ;

L'instruction d'importation suivante doit être ajoutée :

import org.powermock.reflect.Whitebox;

Vous pouvez maintenant transmettre directement l'objet qui possède la méthode privée, le nom de la méthode à appeler et des paramètres supplémentaires comme ci-dessous.

Whitebox.invokeMethod(obj, "privateMethod", "param1");

Aujourd'hui, j'ai poussé une bibliothèque Java pour aider à tester les méthodes et les champs privés.Il a été conçu pour Android, mais il peut vraiment être utilisé pour n'importe quel projet Java.

Si vous avez du code avec des méthodes, des champs ou des constructeurs privés, vous pouvez utiliser Boîte liée.Il fait exactement ce que vous recherchez.Voici ci-dessous un exemple de test qui accède à deux champs privés d'une activité Android pour la tester :

@UiThreadTest
public void testCompute() {

    // Given
    boundBoxOfMainActivity = new BoundBoxOfMainActivity(getActivity());

    // When
    boundBoxOfMainActivity.boundBox_getButtonMain().performClick();

    // Then
    assertEquals("42", boundBoxOfMainActivity.boundBox_getTextViewMain().getText());
}

Boîte liée facilite le test des champs, méthodes et constructeurs privés/protégés.Vous pouvez même accéder à des éléments masqués par héritage.En effet, BoundBox rompt l'encapsulation.Il vous donnera accès à tout cela par la réflexion, MAIS tout est vérifié au moment de la compilation.

Il est idéal pour tester du code existant.Utilisez-le avec précaution.;)

https://github.com/stephanenicolas/boundbox

Tout d'abord, je vais poser cette question :Pourquoi vos membres privés ont-ils besoin de tests isolés ?Sont-ils si complexes, offrant des comportements si compliqués qu'ils nécessitent des tests en dehors de la surface publique ?Il s'agit de tests unitaires, pas de tests de « ligne de code ».Ne vous inquiétez pas pour les petites choses.

S'ils sont si grands, suffisamment grands pour que ces membres privés constituent chacun une « unité » de grande complexité, envisagez de refactoriser ces membres privés hors de cette classe.

Si la refactorisation est inappropriée ou irréalisable, pouvez-vous utiliser le modèle de stratégie pour remplacer l'accès à ces fonctions membres privées/classes membres lors d'un test unitaire ?Lors des tests unitaires, la stratégie fournirait une validation supplémentaire, mais dans les versions, ce serait un simple relais.

J'ai récemment eu ce problème et j'ai écrit un petit outil appelé Crochet, qui évite les problèmes liés à l'utilisation explicite de l'API de réflexion Java, deux exemples :

Méthodes d'appel, par ex. private void method(String s) - par réflexion Java

Method method = targetClass.getDeclaredMethod("method", String.class);
method.setAccessible(true);
return method.invoke(targetObject, "mystring");

Méthodes d'appel, par ex. private void method(String s) - par Picklock

interface Accessible {
  void method(String s);
}

...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.method("mystring");

Paramètres de champs, par ex. private BigInteger amount; - par réflexion Java

Field field = targetClass.getDeclaredField("amount");
field.setAccessible(true);
field.set(object, BigInteger.valueOf(42));

Paramètres de champs, par ex. private BigInteger amount; - par Picklock

interface Accessible {
  void setAmount(BigInteger amount);
}

...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.setAmount(BigInteger.valueOf(42));

Pour Java, j'utiliserais réflexion, car je n'aime pas l'idée de modifier l'accès à un package sur la méthode déclarée juste pour le plaisir de tester.Cependant, je teste généralement simplement les méthodes publiques, ce qui devrait également garantir que les méthodes privées fonctionnent correctement.

vous ne pouvez pas utiliser la réflexion pour obtenir des méthodes privées extérieures à la classe propriétaire, le modificateur privé affecte également la réflexion

Ce n'est pas vrai.Vous pouvez très certainement, comme mentionné dans Réponse de Cem Catikkas.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top