Comment tester les classes abstraites unitaires: étendre avec des talons?


446

Je me demandais comment tester les classes abstraites unitaires et les classes qui étendent les classes abstraites.

Dois-je tester la classe abstraite en l'étendant, en supprimant les méthodes abstraites, puis en testant toutes les méthodes concrètes? Ensuite, ne testez que les méthodes que je remplace et testez les méthodes abstraites dans les tests unitaires pour les objets qui étendent ma classe abstraite?

Dois-je avoir un cas de test abstrait qui peut être utilisé pour tester les méthodes de la classe abstraite et étendre cette classe dans mon cas de test pour les objets qui étendent la classe abstraite?

Notez que ma classe abstraite a quelques méthodes concrètes.

Réponses:


268

Écrivez un objet Mock et utilisez-les uniquement pour les tests. Ils sont généralement très très très minimes (hérités de la classe abstraite) et pas plus. Ensuite, dans votre test unitaire, vous pouvez appeler la méthode abstraite que vous souhaitez tester.

Vous devez tester une classe abstraite qui contient une logique comme toutes les autres classes que vous avez.


9
Merde, je dois dire que c'est la première fois que je suis d'accord avec l'idée d'utiliser une maquette.
Jonathan Allen

5
Vous avez besoin de deux classes, une maquette et un test. La classe fictive étend uniquement les méthodes abstraites de la classe abstraite testée. Ces méthodes peuvent être no-op, retourner null, etc. car elles ne seront pas testées. La classe de test teste uniquement l'API publique non abstraite (c'est-à-dire l'interface implémentée par la classe abstraite). Pour toute classe qui étend la classe Abstract, vous aurez besoin de classes de test supplémentaires car les méthodes abstraites n'étaient pas couvertes.
cyber-moine du

10
Il est évidemment possible de le faire .. mais pour vraiment tester n'importe quelle classe dérivée, vous allez tester cette fonctionnalité de base encore et encore .. ce qui vous amène à avoir un montage de test abstrait afin que vous puissiez prendre en compte cette duplication dans les tests. Tout ça sent! Je recommande fortement de réexaminer pourquoi vous utilisez des classes abstraites en premier lieu et de voir si quelque chose d'autre fonctionnerait mieux.
Nigel Thorne

5
la réponse suivante trop choquée est bien meilleure.
Martin Spamer

22
@MartiSpamer: Je ne dirais pas que cette réponse est surévaluée car elle a été écrite beaucoup plus tôt (2 ans) que la réponse que vous jugez meilleure ci-dessous. Encourageons simplement Patrick car dans le contexte de la publication de cette réponse, c'était génial. Encourageons-nous les uns les autres. Vive
Marvin Thobejane

449

Les classes de base abstraites sont utilisées de deux manières.

  1. Vous spécialisez votre objet abstrait, mais tous les clients utiliseront la classe dérivée via son interface de base.

  2. Vous utilisez une classe de base abstraite pour éliminer la duplication au sein des objets de votre conception, et les clients utilisent les implémentations concrètes via leurs propres interfaces.!


Solution pour 1 - Modèle de stratégie

Option 1

Si vous avez la première situation, vous avez en fait une interface définie par les méthodes virtuelles dans la classe abstraite que vos classes dérivées implémentent.

Vous devriez envisager d'en faire une véritable interface, de changer votre classe abstraite pour qu'elle soit concrète, et de prendre une instance de cette interface dans son constructeur. Vos classes dérivées deviennent alors des implémentations de cette nouvelle interface.

IMotor

Cela signifie que vous pouvez désormais tester votre classe précédemment abstraite à l'aide d'une instance fictive de la nouvelle interface et de chaque nouvelle implémentation via l'interface désormais publique. Tout est simple et testable.


Solution pour 2

Si vous avez la deuxième situation, alors votre classe abstraite fonctionne comme une classe auxiliaire.

AbstractHelper

Jetez un œil aux fonctionnalités qu'il contient. Voyez si une partie peut être poussée sur les objets qui sont manipulés pour minimiser cette duplication. S'il vous reste encore quelque chose, envisagez d'en faire une classe auxiliaire que votre implémentation concrète prendra dans son constructeur et supprimera sa classe de base.

Aide moteur

Cela conduit à nouveau à des classes concrètes qui sont simples et facilement testables.


Comme règle

Privilégiez un réseau complexe d'objets simples à un simple réseau d'objets complexes.

La clé du code testable extensible est de petits blocs de construction et un câblage indépendant.


Mise à jour: comment gérer les mélanges des deux?

Il est possible d'avoir une classe de base remplissant ces deux rôles ... c'est-à-dire: elle a une interface publique et a des méthodes d'assistance protégées. Si tel est le cas, vous pouvez factoriser les méthodes d'assistance en une seule classe (scénario 2) et convertir l'arbre d'héritage en un modèle de stratégie.

Si vous trouvez que certaines méthodes implémentent directement votre classe de base et que d'autres sont virtuelles, vous pouvez toujours convertir l'arbre d'héritage en un modèle de stratégie, mais je le considérerais également comme un bon indicateur que les responsabilités ne sont pas correctement alignées et peuvent besoin de refactoring.


Mise à jour 2: les classes abstraites comme tremplin (2014/06/12)

L'autre jour, j'ai eu une situation où j'ai utilisé l'abstrait, alors j'aimerais explorer pourquoi.

Nous avons un format standard pour nos fichiers de configuration. Cet outil particulier a 3 fichiers de configuration tous dans ce format. Je voulais une classe fortement typée pour chaque fichier de paramètres afin que, grâce à l'injection de dépendances, une classe puisse demander les paramètres qui lui importaient.

J'ai implémenté cela en ayant une classe de base abstraite qui sait analyser les formats de fichiers de paramètres et les classes dérivées qui ont exposé ces mêmes méthodes, mais a encapsulé l'emplacement du fichier de paramètres.

J'aurais pu écrire un "SettingsFileParser" que les 3 classes ont encapsulé, puis délégué à la classe de base pour exposer les méthodes d'accès aux données. J'ai choisi de ne pas le faire encore car cela conduirait à 3 classes dérivées avec plus de code de délégation en elles qu'autre chose.

Cependant ... à mesure que ce code évolue et que les consommateurs de chacune de ces classes de paramètres deviennent plus clairs. Chaque utilisateur de paramètres demandera certains paramètres et les transformera d'une certaine manière (comme les paramètres sont du texte, ils peuvent les envelopper dans des objets de les convertir en nombres, etc.). Dans ce cas, je vais commencer à extraire cette logique dans des méthodes de manipulation de données et à les repousser dans les classes de paramètres fortement typées. Cela conduira à une interface de niveau supérieur pour chaque ensemble de paramètres, qui finit par ne plus savoir qu'il s'agit de «paramètres».

À ce stade, les classes de paramètres fortement typées n'auront plus besoin des méthodes «getter» qui exposent l'implémentation sous-jacente des «paramètres».

À ce stade, je ne voudrais plus que leur interface publique inclue les méthodes d'accesseur des paramètres; je vais donc changer cette classe pour encapsuler une classe d'analyseur de paramètres au lieu d'en dériver.

La classe Abstract est donc: un moyen pour moi d'éviter le code de délégation pour le moment, et un marqueur dans le code pour me rappeler de changer le design plus tard. Je n'y arriverai peut-être jamais, donc ça peut vivre longtemps ... seul le code peut le dire.

Je trouve que cela est vrai avec n'importe quelle règle ... comme "pas de méthodes statiques" ou "pas de méthodes privées". Ils indiquent une odeur dans le code ... et c'est bien. Il vous permet de rechercher l'abstraction que vous avez manquée ... et vous permet de continuer à apporter de la valeur à votre client dans l'intervalle.

J'imagine des règles comme celle-ci définissant un paysage, où le code maintenable vit dans les vallées. Lorsque vous ajoutez un nouveau comportement, c'est comme une pluie tombant sur votre code. Au début, vous le placez partout où il atterrit .. puis vous refactorisez pour permettre aux forces de bonne conception de pousser le comportement jusqu'à ce que tout finisse dans les vallées.


18
C'est une excellente réponse. Beaucoup mieux que les mieux notés. Mais je suppose que seuls ceux qui veulent vraiment écrire du code testable l'apprécieront .. :)
MalcomTucker

22
Je ne peux pas me souvenir de la qualité de la réponse. Cela a totalement changé ma façon de penser les classes abstraites. Merci Nigel.
MalcomTucker

4
Oh non .. un autre principe que je vais devoir repenser! Merci (à la fois sarcastiquement pour l'instant, et non sarcastiquement pour une fois je l'ai assimilé et me sens comme un meilleur programmeur)
Martin Lyne

11
Bonne réponse. Certainement quelque chose à penser ... mais ce que vous dites ne se résume-t-il pas à ne pas utiliser de classes abstraites?
brianestey

32
+1 pour la Règle seule, "Privilégiez un réseau complexe d'objets simples à un simple réseau d'objets complexes."
David Glass

12

Ce que je fais pour les classes abstraites et les interfaces est le suivant: j'écris un test, qui utilise l'objet tel qu'il est concret. Mais la variable de type X (X est la classe abstraite) n'est pas définie dans le test. Cette classe de test n'est pas ajoutée à la suite de tests, mais à ses sous-classes, qui ont une méthode de configuration qui définit la variable sur une implémentation concrète de X. De cette façon, je ne duplique pas le code de test. Les sous-classes du test non utilisé peuvent ajouter plus de méthodes de test si nécessaire.


cela ne cause-t-il pas des problèmes de casting dans la sous-classe? si X a la méthode a et Y hérite de X mais a aussi une méthode b. Lorsque vous sous-classe votre classe de test, ne devez-vous pas convertir votre variable abstraite en Y pour effectuer des tests sur b?
Johnno Nolan

8

Pour effectuer un test unitaire spécifiquement sur la classe abstraite, vous devez le dériver à des fins de test, tester les résultats de base.method () et le comportement prévu lors de l'héritage.

Vous testez une méthode en l'appelant alors testez une classe abstraite en l'implémentant ...


8

Si votre classe abstraite contient une fonctionnalité concrète qui a une valeur commerciale, je la teste généralement directement en créant un double de test qui stoppe les données abstraites, ou en utilisant un cadre de simulation pour le faire pour moi. Laquelle je choisis dépend beaucoup de la nécessité ou non d'écrire des implémentations spécifiques aux tests des méthodes abstraites.

Le scénario le plus courant dans lequel je dois le faire est lorsque j'utilise le modèle de méthode de modèle , comme lorsque je construis une sorte de cadre extensible qui sera utilisé par un tiers. Dans ce cas, la classe abstraite est ce qui définit l'algorithme que je veux tester, il est donc plus logique de tester la base abstraite qu'une implémentation spécifique.

Cependant, je pense qu'il est important que ces tests se concentrent uniquement sur les implémentations concrètes de la logique métier réelle ; vous ne devez pas tester les détails d'implémentation de test de la classe abstraite, car vous vous retrouverez avec des tests fragiles.


6

une façon consiste à écrire un cas de test abstrait qui correspond à votre classe abstraite, puis à écrire des cas de test concrets qui sous-classent votre cas de test abstrait. faites cela pour chaque sous-classe concrète de votre classe abstraite d'origine (c'est-à-dire que votre hiérarchie de cas de test reflète votre hiérarchie de classe). voir Tester une interface dans le livre de recettes junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

voir également Testcase Superclass dans les modèles xUnit: http://xunitpatterns.com/Testcase%20Superclass.html


4

Je plaiderais contre les tests "abstraits". Je pense qu'un test est une idée concrète et n'a pas d'abstraction. Si vous avez des éléments communs, mettez-les dans des méthodes ou des classes d'assistance pour que tout le monde les utilise.

En ce qui concerne le test d'une classe de test abstraite, assurez-vous de vous demander ce que vous testez. Il existe plusieurs approches et vous devez savoir ce qui fonctionne dans votre scénario. Essayez-vous de tester une nouvelle méthode dans votre sous-classe? Faites ensuite interagir vos tests uniquement avec cette méthode. Testez-vous les méthodes de votre classe de base? Ensuite, vous aurez probablement un appareil distinct uniquement pour cette classe et testez chaque méthode individuellement avec autant de tests que nécessaire.


Je ne voulais pas retester le code que j'avais déjà testé, c'est pourquoi je descendais la route des cas de test abstraits. J'essaie de tester toutes les méthodes concrètes de ma classe abstraite en un seul endroit.
Paul Whelan

7
Je suis en désaccord avec l'extraction d'éléments communs aux classes auxiliaires, au moins dans certains (beaucoup?) Cas. Si une classe abstraite contient des fonctionnalités concrètes, je pense qu'il est parfaitement acceptable de tester directement cette fonctionnalité.
Seth Petry-Johnson,

4

C'est le modèle que je suis habituellement en train de configurer un faisceau pour tester une classe abstraite:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

Et la version que j'utilise sous test:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Si les méthodes abstraites sont appelées alors que je ne m'y attendais pas, les tests échouent. Lors de l'organisation des tests, je peux facilement supprimer les méthodes abstraites avec des lambdas qui effectuent des assertions, lèvent des exceptions, renvoient des valeurs différentes, etc.


3

Si les méthodes concrètes invoquent l'une des méthodes abstraites, cette stratégie ne fonctionnera pas et vous souhaitez tester chaque comportement de classe enfant séparément. Sinon, l'étendre et écraser les méthodes abstraites comme vous l'avez décrit devrait être correct, à condition que les méthodes concrètes de la classe abstraite soient découplées des classes enfants.


2

Je suppose que vous pourriez vouloir tester la fonctionnalité de base d'une classe abstraite ... Mais vous feriez probablement mieux en étendant la classe sans remplacer aucune méthode, et en faisant un effort minimum pour les méthodes abstraites.


2

L'une des principales motivations pour utiliser une classe abstraite est d'activer le polymorphisme dans votre application - c'est-à-dire: vous pouvez remplacer une version différente au moment de l'exécution. En fait, c'est à peu près la même chose que d'utiliser une interface, sauf que la classe abstraite fournit une plomberie commune, souvent appelée modèle de modèle .

Du point de vue des tests unitaires, il y a deux choses à considérer:

  1. Interaction de votre classe abstraite avec les classes associées . L'utilisation d'un cadre de test simulé est idéale pour ce scénario car il montre que votre classe abstraite joue bien avec les autres.

  2. Fonctionnalité des classes dérivées . Si vous avez écrit une logique personnalisée pour vos classes dérivées, vous devez tester ces classes isolément.

edit: RhinoMocks est un cadre de test de simulation génial qui peut générer des objets simulés lors de l'exécution en dérivant dynamiquement de votre classe. Cette approche peut vous faire économiser d'innombrables heures de classes dérivées à codage manuel.


2

Tout d'abord, si la classe abstraite contenait une méthode concrète, je pense que vous devriez le faire en tenant compte de cet exemple

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

Si une classe abstraite est appropriée pour votre implémentation, testez (comme suggéré ci-dessus) une classe concrète dérivée. Vos hypothèses sont correctes.

Pour éviter toute confusion future, sachez que cette classe de test concrète n'est pas un simulacre, mais un faux .

En termes stricts, une maquette est définie par les caractéristiques suivantes:

  • Une maquette est utilisée à la place de chaque dépendance de la classe de sujet testée.
  • Une maquette est une pseudo-implémentation d'une interface (vous vous souvenez peut-être qu'en règle générale, les dépendances doivent être déclarées en tant qu'interfaces; la testabilité en est l'une des principales raisons)
  • Les comportements des membres de l'interface de la maquette - qu'il s'agisse de méthodes ou de propriétés - sont fournis au moment du test (là encore, en utilisant un framework de simulation). De cette façon, vous évitez de coupler l'implémentation testée avec l'implémentation de ses dépendances (qui devraient toutes avoir leurs propres tests discrets).

1

Suite à la réponse de @ patrick-desjardins, j'ai implémenté le résumé et sa classe d'implémentation ainsi @Testque:

Classe abstraite - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Comme les classes abstraites ne peuvent pas être instanciées, mais elles peuvent être sous - classées , la classe concrète DEF.java est la suivante:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

Classe @Test pour tester à la fois la méthode abstraite et non abstraite:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.