Est-ce une mauvaise pratique d’utiliser les tests Reflection in Unit?

Au cours des dernières années, j’ai toujours pensé qu’en Java, Reflection est largement utilisé lors des tests unitaires. Comme certaines des variables / méthodes à vérifier sont privées, il est nécessaire d’en lire les valeurs. J’ai toujours pensé que l’API Reflection était également utilisée à cette fin.

La semaine dernière, j’ai dû tester des paquets et donc écrire des tests JUnit. Comme toujours, j’ai utilisé Reflection pour accéder à des champs et méthodes privés. Mais mon superviseur qui a vérifié le code n’était pas vraiment satisfait de cela et m’a dit que l’API Reflection n’était pas destinée à être utilisée pour un tel “piratage”. Au lieu de cela, il a suggéré de modifier la visibilité dans le code de production.

Est-ce vraiment une mauvaise pratique d’utiliser Reflection? Je ne peux pas vraiment croire que-

Edit: J’aurais dû mentionner que je devais que tous les tests soient dans un paquet séparé appelé test (utiliser une visibilité protégée, par exemple, n’était pas une solution possible)

IMHO Reflection ne devrait vraiment être qu’un dernier recours, réservé au cas particulier du code hérité de test unitaire ou d’une API que vous ne pouvez pas modifier. Si vous testez votre propre code, le fait que vous ayez besoin d’utiliser Reflection signifie que votre conception n’est pas testable, vous devriez donc y remédier au lieu de recourir à Reflection.

Si vous avez besoin d’accéder à des membres privés dans vos tests unitaires, cela signifie généralement que la classe en question possède une interface inappropriée et / ou tente d’en faire trop. Donc, soit son interface doit être révisée, soit un code doit être extrait dans une classe distincte, où ces méthodes / accesseurs de champ problématiques peuvent être rendus publics.

Notez que l’utilisation de Reflection en général se traduit par un code qui, en plus d’être plus difficile à comprendre et à maintenir, est également plus fragile. Il y a toute une série d’erreurs qui, dans le cas normal, seraient détectées par le compilateur, mais avec Reflection, elles ne sont que des exceptions d’exécution.

Mise à jour: comme @tackline l’a noté, cela concerne uniquement l’utilisation de Reflection dans son propre code de test, et non les composants internes du framework de test. JUnit (et probablement tous les autres frameworks similaires) utilise la reflection pour identifier et appeler vos méthodes de test – ceci est un usage justifié et localisé de la reflection. Il serait difficile ou impossible de fournir les mêmes fonctionnalités et commodité sans utiliser Reflection. OTOH il est complètement encapsulé dans la mise en œuvre du cadre, donc il ne complique pas ou ne compromet pas notre propre code de test.

Il est vraiment néfaste de modifier la visibilité d’une API de production uniquement à des fins de test. Cette visibilité est susceptible d’être définie à sa valeur actuelle pour des raisons valables et ne doit pas être modifiée.

L’utilisation de la reflection pour les tests unitaires est généralement satisfaisante. Bien sûr, vous devez concevoir vos classes pour qu’elles soient testables , afin de réduire la reflection.

Spring par exemple a ReflectionTestUtils . Mais son but est de créer des moqueries de dépendances, où le spring était censé les injecter.

Le sujet est plus profond que “do & don’t”, et concerne ce qui devrait être testé – si l’état interne des objects doit être testé ou non; si nous devrions nous permettre de remettre en question la conception de la classe à tester; etc.

Du sharepoint vue de TDD – Test Driven Design – c’est une mauvaise pratique. Je sais que vous n’avez pas étiqueté ce TDD, ni demandé spécifiquement à ce sujet, mais TDD est une bonne pratique et cela va à l’encontre de son grain.

Dans TDD, nous utilisons nos tests pour définir l’interface de la classe – l’interface publique – et par conséquent, nous écrivons des tests qui n’interagissent directement qu’avec l’interface publique. Nous sums concernés par cette interface; les niveaux d’access appropriés constituent une partie importante de la conception, une partie importante d’un bon code. Si vous avez besoin de tester quelque chose de privé, c’est généralement, selon mon expérience, une odeur de design.

Je considérerais cela comme une mauvaise pratique, mais le simple fait de changer la visibilité dans le code de production n’est pas une bonne solution, il faut en chercher la cause. Soit vous avez une API non testable (c’est-à-dire que l’API n’expose pas suffisamment pour un test afin que vous puissiez la gérer), donc vous cherchez à tester l’état privé à la place, ou vos tests sont trop liés à votre implémentation, ce qui les rendra seule utilisation marginale lorsque vous refactorez.

Sans en savoir plus sur votre cas, je ne peux pas vraiment en dire plus, mais il est en effet considéré comme une mauvaise pratique d’utiliser la reflection . Personnellement, je préférerais que le test soit une classe interne statique de la classe testée plutôt que de recourir à la reflection (si par exemple la partie non testable de l’API n’était pas sous mon contrôle), le même package que le code de production qu’avec l’utilisation de la reflection.

EDIT: En réponse à votre édition, cela est une pratique au moins aussi mauvaise que la reflection, probablement pire. La façon dont il est généralement géré consiste à utiliser le même package, mais conservez les tests dans une structure de répertoire distincte. Si les tests unitaires n’appartiennent pas au même paquet que la classe testée, je ne sais pas ce que cela fait.

Quoi qu’il en soit, vous pouvez toujours contourner ce problème en utilisant protected (malheureusement pas package-private qui est vraiment idéal pour cela) en testant une sous-classe comme ceci:

  public class ClassUnderTest { protect void methodExposedForTesting() {} } 

Et à l’intérieur de votre test unitaire

 class ClassUnderTestTestable extends ClassUnderTest { @Override public void methodExposedForTesting() { super.methodExposedForTesting() } } 

Et si vous avez un constructeur protégé:

 ClassUnderTest test = new ClassUnderTest(){}; 

Je ne recommande pas nécessairement ce qui précède pour les situations normales, mais les ressortingctions auxquelles vous êtes invité à travailler et non pas les “meilleures pratiques”.

Je second la pensée de Bozho:

Il est vraiment néfaste de modifier la visibilité d’une API de production uniquement à des fins de test.

Mais si vous essayez de faire la chose la plus simple qui puisse fonctionner, il serait préférable d’écrire un résumé de l’API Reflection, du moins lorsque votre code est encore nouveau / en cours de modification. Je veux dire, le fardeau de changer manuellement les appels réfléchis à partir de votre test chaque fois que vous modifiez le nom de la méthode, ou les parameters, est trop lourd pour IMHO et le mauvais focus.

Je suis tombé dans le piège de l’accessibilité juste pour tester et accéder par inadvertance à la méthode was-private à partir d’un autre code de production. J’ai pensé à dp4j.jar : il injecte le code de l’API Reflection ( style Lombok ) code de production ET n’écrivez pas l’API Reflection vous-même; dp4j remplace votre access direct dans le test unitaire par une API de reflection équivalente au moment de la compilation. Voici un exemple de dp4j avec JUnit .

Je pense que votre code doit être testé de deux manières. Vous devriez tester vos méthodes publiques via Unit Test et cela fera office de test de boîte noire. Étant donné que votre code est divisé en fonctions gérables (bonne conception), vous voudrez tester les pièces individuelles avec reflection pour vous assurer qu’elles fonctionnent indépendamment du processus, la seule façon d’y penser serait de réfléchir car ils sont privés

Au moins, c’est ma pensée dans le processus de test unitaire.

Pour append à ce que d’autres ont dit, considérez ce qui suit:

 //in your JUnit code... public void testSquare() { Class classToTest = SomeApplicationClass.class; Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class}); Ssortingng result = (Ssortingng)privateMethod.invoke(new Integer(3)); assertEquals("9", result); } 

Ici, nous utilisons la reflection pour exécuter la méthode privée SomeApplicationClass.computeSquare (), en passant un Integer et en renvoyant un résultat Ssortingng. Cela se traduit par un test JUnit qui se comstackra bien mais échouera lors de l’exécution si l’une des situations suivantes se produit:

  • Le nom de la méthode “computeSquare” est renommé
  • La méthode prend en compte un type de paramètre différent (par exemple, changer Integer en Long)
  • Le nombre de parameters change (par exemple en passant dans un autre paramètre)
  • Le type de retour de la méthode change (peut-être de Ssortingng à Integer)

Au lieu de cela, s’il n’y a pas de moyen facile de prouver que le computeSquare a fait ce qu’il est censé faire à travers l’API publique, alors votre classe essaiera probablement de faire beaucoup. Tirez cette méthode dans une nouvelle classe qui vous donne le test suivant:

 public void testSquare() { Ssortingng result = new NumberUtils().computeSquare(new Integer(3)); assertEquals("9", result); } 

Maintenant (surtout lorsque vous utilisez les outils de refactoring disponibles dans les IDE modernes), changer le nom de la méthode n’a aucun effet sur votre test (votre IDE ayant également refait le test JUnit), tout en changeant le type de paramètre, le nombre de Les parameters ou le type de retour de la méthode signaleront une erreur de compilation dans votre test Junit, ce qui signifie que vous ne pourrez pas archiver un test JUnit qui comstack mais échoue à l’exécution.

Mon dernier point est que, parfois, en particulier lorsque vous travaillez avec du code hérité, et que vous devez append de nouvelles fonctionnalités, il peut ne pas être facile de le faire dans une classe distincte testable bien écrite. Dans ce cas, je vous recommande d’isoler les nouvelles modifications de code apscopes aux méthodes de visibilité protégée que vous pouvez exécuter directement dans votre code de test JUnit. Cela vous permet de commencer à construire une base de code de test. Finalement, vous devez restructurer la classe et extraire vos fonctionnalités ajoutées, mais en attendant, la visibilité protégée sur votre nouveau code peut parfois être votre meilleure solution en termes de testabilité sans refactorisation majeure.