web-dev-qa-db-fra.com

Est-ce une mauvaise pratique d'utiliser la réflexion dans les tests unitaires?

Au cours des dernières années, j'ai toujours pensé qu'en Java, la réflexion était largement utilisée lors des tests unitaires. Étant donné que certaines des variables/méthodes qui doivent être vérifiées sont privées, il est en quelque sorte nécessaire d'en lire les valeurs. J'ai toujours pensé que l'API Reflection est également utilisée à cette fin.

La semaine dernière, j'ai dû tester certains packages et donc écrire des tests JUnit. Comme toujours, j'ai utilisé Reflection pour accéder aux 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 package séparé appelé test (donc utiliser la visibilité protégée par exemple n'était pas une solution possible aussi)

91

IMHO Reflection ne devrait vraiment être qu'un dernier recours, réservé au cas particulier du test d'unité de code hérité ou d'une API que vous ne pouvez pas changer. Si vous testez votre propre code, le fait que vous devez utiliser Reflection signifie que votre conception n'est pas testable, vous devez donc corriger cela 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 a une interface inappropriée et/ou essaie d'en faire trop. Donc, soit son interface doit être révisée, soit du 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 entraîne un code qui, en plus d'être plus difficile à comprendre et à maintenir, est également plus fragile. Il existe tout un ensemble d'erreurs qui, dans le cas normal, seraient détectées par le compilateur, mais avec Reflection, elles n'apparaissent que comme des exceptions d'exécution.

pdate: comme l'a noté @tackline, cela ne concerne que l'utilisation de Reflection dans son propre code de test, pas les internes du framework de test. JUnit (et probablement tous les autres cadres similaires) utilise la réflexion pour identifier et appeler vos méthodes de test - il s'agit d'une utilisation justifiée et localisée de la réflexion. Il serait difficile, voire impossible, de fournir les mêmes fonctionnalités et commodités sans utiliser Reflection. OTOH il est complètement encapsulé dans l'implémentation du framework, donc il ne complique pas ou ne compromet pas notre propre code de test.

70
Péter Török

Il est vraiment mauvais de modifier la visibilité d'une API de production juste pour le plaisir de tester. Cette visibilité est susceptible d'être réglée sur sa valeur actuelle pour des raisons valables et ne doit pas être modifiée.

L'utilisation de la réflexion pour les tests unitaires est généralement très bien. Bien sûr, vous devez concevoir vos classes pour la testabilité , afin que moins de réflexion soit requise.

Spring par exemple a ReflectionTestUtils. Mais son but est de se moquer des dépendances, où le printemps était censé les injecter.

Le sujet est plus profond que "faire et ne pas faire" et concerne ce qui doit être testé - si l'état interne des objets doit être testé ou ne pas; si nous devons nous permettre de remettre en question la conception de la classe testée; etc.

63
Bozho

Du point de vue de TDD - Test Driven Design - c'est une mauvaise pratique. Je sais que vous n'avez pas marqué ce TDD, ni demandé spécifiquement à ce sujet, mais le 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 public - et donc nous écrivons des tests qui n'interagissent directement qu'avec l'interface publique. Nous nous intéressons à cette interface; des niveaux d'accès appropriés sont une partie importante de la conception, une partie importante d'un bon code. Si vous devez tester quelque chose de privé, c'est généralement, selon mon expérience, une odeur de design.

29
Carl Manaster

Je considérerais cela comme une mauvaise pratique, mais changer simplement 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 qu'un test soit maîtrisé), vous cherchez donc à tester l'état privé à la place, soit vos tests sont trop couplés à votre implémentation, ce qui les rendra utilisation marginale seulement lorsque vous refactorisez.

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 réflexion . Personnellement, je préférerais faire du test une classe interne statique de la classe testée plutôt que de recourir à la réflexion (si par exemple la partie non testable de l'API n'était pas sous mon contrôle), mais certains endroits auront un problème plus important avec le code de test étant le même package que le code de production qu'avec l'utilisation de la réflexion.

EDIT: En réponse à votre édition, que est au moins une pratique aussi mauvaise que l'utilisation de 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épertoires distincte. Si les tests unitaires n'appartiennent pas au même package 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 restrictions qui vous sont demandées de travailler et pas les "meilleures pratiques" déjà.

7
Yishai

J'appuie la pensée de Bozho:

Il est vraiment mauvais 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 pourrait fonctionner alors il pourrait être préférable d'écrire le passe-partout de l'API Reflection, au moins pendant que votre code est encore nouveau/en train de changer. Je veux dire, le fardeau de changer manuellement les appels reflétés à partir de votre test chaque fois que vous changez le nom de la méthode ou les paramètres, est à mon humble avis un fardeau trop lourd et une mauvaise focalisation.

Après être tombé dans le piège de la détente de l'accessibilité juste pour le tester, puis accéder par inadvertance à la méthode was-private à partir d'un autre code de production auquel j'ai pensé dp4j.jar : il injecte le code API Reflection ( Lombok -style ) afin de ne pas modifier le code de production ET de ne pas écrire l'API Reflection vous-même; dp4j remplace votre accès direct dans le test unitaire par l'API de réflexion équivalente au moment de la compilation. Voici un exemple de dp4j avec JUnit .

6
simpatico

Je pense que votre code devrait être testé de deux manières. Vous devez tester vos méthodes publiques via Unit Test et cela agira comme notre test de boîte noire. Étant donné que votre code est divisé en fonctions gérables (bonne conception), vous voudrez tester les pièces individuellement avec réflexion pour vous assurer qu'elles fonctionnent indépendamment du processus, la seule façon que je puisse penser de le faire serait avec la réflexion puisque ils sont privés.

C'est du moins ma pensée dans le processus de test unitaire.

4
Richard R

Pour ajouter à ce que les 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});
   String result = (String)privateMethod.invoke(new Integer(3));
   assertEquals("9", result);
}

Ici, nous utilisons la réflexion pour exécuter la méthode privée SomeApplicationClass.computeSquare (), en passant un Integer et en renvoyant un résultat String. Cela se traduit par un test JUnit qui se compilera correctement mais échouera pendant l'exécution si l'un des événements suivants se produit:

  • Le nom de la méthode "computeSquare" est renommé
  • La méthode utilise un type de paramètre différent (par exemple, changer Integer en Long)
  • Le nombre de paramètres change (par exemple en passant un autre paramètre)
  • Le type de retour de la méthode change (peut-être de String à Integer)

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

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

Maintenant (surtout lorsque vous utilisez les outils de refactoring disponibles dans les IDE modernes), la modification du nom de la méthode n'a aucun effet sur votre test (car votre IDE aura également refactorisé le test JUnit également) , tout en changeant le type de paramètre, le nombre de paramètres ou le type de retour de la méthode signalera une erreur de compilation dans votre test Junit, ce qui signifie que vous ne archiverez pas un test JUnit qui se compile mais échoue au moment de l'exécution.

Mon dernier point est que parfois, en particulier lorsque vous travaillez dans du code hérité, et que vous devez ajouter de nouvelles fonctionnalités, il peut ne pas être facile de le faire dans une classe distincte testable et bien écrite. Dans ce cas, ma recommandation serait d'isoler les nouvelles modifications de code aux méthodes de visibilité protégées 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 devriez refactoriser la classe et extraire vos fonctionnalités ajoutées, mais en attendant, une visibilité protégée sur votre nouveau code peut parfois être votre meilleure voie à suivre en termes de testabilité sans refactorisation majeure.

3
Chris Knight