Question

Plusieurs fois, je suis impliqué dans la conception / mise en œuvre d'API, je suis confronté à ce dilemme.

Je suis un très fort partisan de cachette d'information Et essayez d'utiliser diverses techniques pour cela, y compris, mais sans s'y limiter, les classes intérieures, les méthodes privées, les qualifications-privés de package, etc.

Le problème avec ces techniques est qu'ils ont tendance à empêcher une bonne testabilité. Et bien que certaines de ces techniques puissent être résolues (par exemple, la prime de package en mettant une classe dans le même package), d'autres sont Pas si facile à aborder et nécessite une magie de réflexion ou d'autres astuces.

Regardons l'exemple en béton:

public class Foo {
   SomeType attr1;
   SomeType attr2;
   SomeType attr3;

   public void someMethod() {
      // calculate x, y and z
      SomethingThatExpectsMyInterface something = ...;
      something.submit(new InnerFoo(x, y, z));
   }

   private class InnerFoo implements MyInterface {
      private final SomeType arg1;
      private final SomeType arg2;
      private final SomeType arg3;

      InnerFoo(SomeType arg1, SomeType arg2, SomeType arg3) {
         this.arg1 = arg1;
         this.arg2 = arg2;
         this.arg3 = arg3;
      }

      @Override
      private void methodOfMyInterface() {
         //has access to attr1, attr2, attr3, arg1, arg2, arg3
      }
   }
}

Il y a de fortes raisons de ne pas exposer InnerFoo - Aucune autre classe, la bibliothèque ne devrait y avoir accès car elle ne définit aucun contrat public et l'auteur ne voulait délibérément pas qu'il soit accessible. Cependant, pour le rendre 100% TDD-KOSHER et accessible sans aucune astuce de réflexion, InnerFoo devrait être refactorisé comme ceci:

private class OuterFoo implements MyInterface {
   private final SomeType arg1;
   private final SomeType arg2;
   private final SomeType arg3;
   private final SomeType attr1;
   private final SomeType attr2;
   private final SomeType attr3;

   OuterFoo(SomeType arg1, SomeType arg2, SomeType arg3, SomeType attr1, SomeType attr2, SomeType attr3) {
      this.arg1 = arg1;
      this.arg2 = arg2;
      this.arg3 = arg3;
      this.attr1 = attr1;
      this.attr2 = attr2;
      this.attr3 = attr3;
   }

   @Override
   private void methodOfMyInterface() {
      //can be unit tested without reflection magic
   }
}

Cela ne concerne que 3 attrs, mais il est assez raisonnable d'avoir 5-6 et le OuterFoo Le constructeur devrait alors accepter les paramètres 8-10! Ajoutez des getters sur le dessus et vous avez déjà 100 lignes de code complètement inutile (des getters seraient également nécessaires pour obtenir ces attributions pour les tests). Oui, je pourrais améliorer la situation en fournissant un modèle de générateur, mais je pense que ce n'est pas seulement une sur-ingénierie, mais aussi pour vaincre le but du TDD lui-même!

Une autre solution pour ce problème serait d'exposer une méthode protégée pour la classe Foo, prolongez-le dans FooTest et obtenir les données requises. Encore une fois, je pense que c'est aussi une mauvaise approche parce que protected méthode définit un contrat Et en l'exposant, je l'ai maintenant implicitement signé.

Ne vous méprenez pas. J'aime écrire du code testable. J'adore les API concises, propres, les blocs de code court, la lisibilité, etc. Mais ce que je n'aime pas, c'est faire des sacrifices en ce qui concerne la cachette d'information juste parce qu'il est plus facile de tester unité.

Quelqu'un peut-il fournir des réflexions à ce sujet (en général, et en particulier)? Y a-t-il d'autres, de meilleures solutions pour un exemple donné?

Était-ce utile?

La solution

Je pense que vous devriez reconsidérer en utilisant la réflexion.

Il a ses propres inconvénients, mais s'il vous permet de maintenir le modèle de sécurité que vous souhaitez sans code factice, cela peut être une bonne chose. La réflexion n'est souvent pas requise, mais parfois il n'y a pas de bon substitut.

Une autre approche de la cachette d'information consiste à traiter la classe / l'objet comme une boîte noire et à ne pas accéder aux méthodes non publiques (bien que cela puisse permettre aux tests de passer pour les "mauvaises" raisons, c'est-à-dire que la réponse est bonne mais pour les mauvaises raisons.)

Autres conseils

Ma réponse incontournable pour ce type de chose est un "proxy de test". Dans votre package de test, dérivez une sous-classe de votre système testé, qui contient des accessoires "pass-through" aux données protégées.

Avantages:

  • Vous pouvez tester ou si vous vous moquez directement des méthodes que vous ne souhaitez pas rendre publiques.
  • Étant donné que le proxy de test vit dans le package de test, vous pouvez vous assurer qu'il n'est jamais utilisé dans le code de production.
  • Un proxy de test nécessite beaucoup moins de modifications du code afin de le rendre testable que si vous testiez directement la classe.

Désavantages:

  • La classe doit être héritable (non final)
  • Tous les membres cachés auxquels vous devez accéder ne peuvent pas être privés; Protégé est le meilleur que vous puissiez faire.
  • Ce n'est pas strictement TDD; TDD se prêterait à des modèles qui ne nécessitaient pas de proxy de test en premier lieu.
  • Ce n'est pas strictement même les tests unitaires, car à un certain niveau, vous dépendez de «l'intégration» entre le proxy et le SUT réel.

En bref, cela devrait normalement être une rareté. J'ai tendance à l'utiliser uniquement pour les éléments d'interface utilisateur, où la meilleure pratique (et le comportement par défaut de nombreux IDE) consiste à déclarer des contrôles d'interface utilisateur imbriqués comme inaccessibles de l'extérieur de la classe. Certainement une bonne idée, vous pouvez donc contrôler la façon dont les appelants obtiennent des données de l'interface utilisateur, mais cela rend également difficile de donner au contrôle quelques valeurs connues pour tester cette logique.

Je ne vois pas comment les informations se cachent, dans le résumé, réduit votre testabilité.

Si vous injectiez le SomethingThatExpectsMyInterface utilisé dans cette méthode plutôt que de la construire directement:

public void someMethod() {
   // calculate x, y and z
   SomethingThatExpectsMyInterface something = ...;
   something.submit(new InnerFoo(x, y, z));
}

Ensuite, dans un test unitaire, vous pouvez injecter cette classe avec une version simulée de SomethingThatExpectsMyInterface et affirme facilement ce qui se passe lorsque vous appelez someMethod() avec des entrées différentes - que le mockSomething reçoit des arguments de certaines valeurs.

Je pense que vous avez peut-être sur-simplifié cet exemple car Innerfoo ne peut pas être une classe privée si SomethingThatExpectsMyInterface reçoit des arguments de ce type.

La "cachette d'information" ne signifie pas nécessairement que les objets que vous passez entre vos classes doivent être un secret - juste que vous n'avez pas besoin de code externe en utilisant cette classe pour être conscient des détails des détails InnerFoo ou les autres détails de la façon dont cette classe communique avec les autres.

SomethingThatExpectsMyInterface peut être testé à l'extérieur Foo, droit? Vous pouvez appeler son submit() Méthode avec votre propre classe de test qui implémente MyInterface. Cette unité est donc prise en charge. Maintenant, vous testez Foo.someMethod() avec cette unité bien testée et votre classe intérieure non testée. Ce n'est pas idéal - mais ce n'est pas trop mal. Pendant que vous testez someMethod(), vous testez implicitement la classe intérieure. Je sais que ce n'est pas pur TDD, selon certaines normes strictes, mais je considérerais cela suffisant. Vous n'écrivez pas une ligne de la classe intérieure, sauf pour satisfaire un test d'échec; qu'il y a un seul niveau d'indirection entre votre test et le code testé ne constitue pas un gros problème.

Dans votre exemple, il semble que la classe FOO ait vraiment besoin d'un collaborateur Innerfoo.

À mon avis, la tension entre la cachette des informations et la testabilité est résolue par "composite plus simple que la somme de ses pièces" devise.

FOO est une façade sur un composite (juste innerfoo dans votre cas, mais n'a pas d'importance.) L'objet de façade doit être testé sur son comportement prévu. Si vous pensez que le code d'objet InnerFoo n'est pas suffisamment entraîné par les tests sur le comportement de FOO, vous devriez considérer ce que InnerFoo représente dans votre conception. Il se peut que vous manquiez un concept de design. Lorsque vous le trouvez, le nommez et définissez ses responsabilités, vous pouvez tester son comportement séparément.

Ce que je n'aime pas, c'est faire des sacrifices en ce qui concerne la cachette d'information

Tout d'abord, travaillez avec Python pendant quelques années.

private n'est pas particulièrement utile. Il rend l'extension de la classe difficile et rend les tests difficiles.

Pensez à repenser votre position sur "cacher".

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