web-dev-qa-db-fra.com

Comment ajouter une couverture de test à un constructeur privé?

C'est le code:

package com.XXX;
public final class Foo {
  private Foo() {
    // intentionally empty
  }
  public static int bar() {
    return 1;
  }
}

C'est le test:

package com.XXX;
public FooTest {
  @Test 
  void testValidatesThatBarWorks() {
    int result = Foo.bar();
    assertEquals(1, result);
  }
  @Test(expected = Java.lang.IllegalAccessException.class)
  void testValidatesThatClassFooIsNotInstantiable() {
    Class cls = Class.forName("com.XXX.Foo");
    cls.newInstance(); // exception here
  }
}

Fonctionne bien, la classe est testée. Mais Cobertura dit qu'il n'y a aucune couverture de code du constructeur privé de la classe. Comment pouvons-nous ajouter une couverture de test à un tel constructeur privé?

103
yegor256

Eh bien, il y a des façons d'utiliser potentiellement la réflexion, etc. - mais est-ce vraiment utile? C'est un constructeur qui devrait ne jamais s'appeler, n'est-ce pas?

S'il y a une annotation ou quelque chose de similaire que vous pouvez ajouter à la classe pour que Cobertura comprenne qu'elle ne s'appellera pas, faites-le: je ne pense pas que ça vaille la peine de passer par des cerceaux pour ajouter une couverture artificiellement.

EDIT: S'il n'y a aucun moyen de le faire, vivez avec une couverture légèrement réduite. Rappelez-vous que la couverture est censée être quelque chose d’utile pour vous - vous devriez être responsable de l’outil, et non l’inverse.

77
Jon Skeet

Je ne suis pas tout à fait d'accord avec Jon Skeet. Je pense que si vous pouvez obtenir une victoire facile pour vous couvrir et éliminer le bruit dans votre rapport de couverture, vous devriez le faire. Dites à votre outil de couverture d'ignorer le constructeur ou mettez l'idéalisme de côté et écrivez le test suivant pour en finir:

@Test
public void testConstructorIsPrivate() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  Constructor<Foo> constructor = Foo.class.getDeclaredConstructor();
  assertTrue(Modifier.isPrivate(constructor.getModifiers()));
  constructor.setAccessible(true);
  constructor.newInstance();
}
131
Javid Jamae

Bien que ce ne soit pas nécessairement pour la couverture, j'ai créé cette méthode pour vérifier que la classe d'utilitaire est bien définie et couvrir également un peu la couverture.

/**
 * Verifies that a utility class is well defined.
 * 
 * @param clazz
 *            utility class to verify.
 */
public static void assertUtilityClassWellDefined(final Class<?> clazz)
        throws NoSuchMethodException, InvocationTargetException,
        InstantiationException, IllegalAccessException {
    Assert.assertTrue("class must be final",
            Modifier.isFinal(clazz.getModifiers()));
    Assert.assertEquals("There must be only one constructor", 1,
            clazz.getDeclaredConstructors().length);
    final Constructor<?> constructor = clazz.getDeclaredConstructor();
    if (constructor.isAccessible() || 
                !Modifier.isPrivate(constructor.getModifiers())) {
        Assert.fail("constructor is not private");
    }
    constructor.setAccessible(true);
    constructor.newInstance();
    constructor.setAccessible(false);
    for (final Method method : clazz.getMethods()) {
        if (!Modifier.isStatic(method.getModifiers())
                && method.getDeclaringClass().equals(clazz)) {
            Assert.fail("there exists a non-static method:" + method);
        }
    }
}

J'ai placé le code complet et des exemples dans https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test

74

J'avais rendu privé le constructeur de ma classe de fonctions utilitaires statiques, pour satisfaire CheckStyle. Mais comme l'affiche originale, Cobertura s'est plaint du test. Au début, j'ai essayé cette approche, mais cela n'affecte pas le rapport de couverture car le constructeur n'est jamais réellement exécuté. Donc, vraiment, tous ces tests portent sur le fait que le constructeur reste privé - et ceci est rendu redondant par la vérification de l'accessibilité dans le test suivant.

@Test(expected=IllegalAccessException.class)
public void testConstructorPrivate() throws Exception {
    MyUtilityClass.class.newInstance();
    fail("Utility class constructor should be private");
}

J'y suis allé avec la suggestion de Javid Jamae et ai utilisé la réflexion, mais j'ai ajouté des assertions pour surprendre quiconque se mêlait de la classe à tester (et ai nommé le test pour indiquer les niveaux élevés du mal).

@Test
public void evilConstructorInaccessibilityTest() throws Exception {
    Constructor[] ctors = MyUtilityClass.class.getDeclaredConstructors();
    assertEquals("Utility class should only have one constructor",
            1, ctors.length);
    Constructor ctor = ctors[0];
    assertFalse("Utility class constructor should be inaccessible", 
            ctor.isAccessible());
    ctor.setAccessible(true); // obviously we'd never do this in production
    assertEquals("You'd expect the construct to return the expected type",
            MyUtilityClass.class, ctor.newInstance().getClass());
}

C'est exagéré, mais je dois avouer que j'aime le sentiment de flou qui caractérise une couverture de méthode à 100%.

18
Ben Hardy

Avec Java 8, il est possible de trouver une autre solution.

Je suppose que vous voulez simplement créer une classe d’utilitaire avec quelques méthodes statiques publiques. Si vous pouvez utiliser Java 8, alors vous pouvez utiliser interface à la place.

package com.XXX;

public interface Foo {

  public static int bar() {
    return 1;
  }
}

Il n'y a pas de constructeur et rien à redire de Cobertura. Maintenant, vous devez tester uniquement les lignes qui vous intéressent vraiment.

9
Arnost Valicek

Le test de code qui ne fait rien, consiste à obtenir une couverture de code à 100% et à remarquer le moment où la couverture de code diminue. Sinon, on pourrait toujours penser, hé, je n'ai plus de couverture de code à 100% mais c'est probablement à cause de mes constructeurs privés. Cela permet de repérer facilement les méthodes non testées sans avoir à vérifier qu'il s'agit simplement d'un constructeur privé. Au fur et à mesure que votre code grandit, vous vous sentirez vraiment bien au chaud à 100% au lieu de 99%.

IMO, il est préférable d'utiliser la réflexion ici car sinon, vous devrez soit obtenir un meilleur outil de couverture de code qui ignore ces constructeurs, soit indiquer à l'outil de couverture de code d'ignorer la méthode (peut-être une annotation ou un fichier de configuration), car vous seriez bloqué. avec un outil de couverture de code spécifique.

Dans un monde parfait, tous les outils de couverture de code ignoreraient les constructeurs privés appartenant à une classe finale, car le constructeur est là en tant que mesure "de sécurité", rien d'autre :)
Je voudrais utiliser ce code:

    @Test
    public void callPrivateConstructorsForCodeCoverage() throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException
    {
        Class<?>[] classesToConstruct = {Foo.class};
        for(Class<?> clazz : classesToConstruct)
        {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            assertNotNull(constructor.newInstance());
        }
    }
5
jontejj

Les versions les plus récentes de Cobertura ont un support intégré pour ignorer les getters/setters/constructeurs triviaux:

https://github.com/cobertura/cobertura/wiki/Ant-Task-Reference#ignore-trivial

Ignorer Trivial

Ignorer trivial permet d'exclure des constructeurs/méthodes contenant une ligne de code. Certains exemples incluent un appel à un super constrctor uniquement, des méthodes getter/setter, etc. Pour inclure l'argument trivial ignore, ajoutez ce qui suit:

<cobertura-instrument ignoreTrivial="true" />

ou dans une construction Gradle:

cobertura {
    coverageIgnoreTrivial = true
}
5
Mike Buhot

Ne pas Quel est l'intérêt de tester un constructeur vide? Depuis cobertura 2.0, il existe une option pour ignorer de tels cas triviaux (avec les setters/getters), vous pouvez l'activer dans maven en ajoutant une section de configuration à cobertura mav plugin:

<configuration>
  <instrumentation>
    <ignoreTrivial>true</ignoreTrivial>                 
  </instrumentation>
</configuration>

Sinon, vous pouvez utiliser annotations de couverture : @CoverageIgnore.

4
Krzysztof Krasoń

Enfin, il y a une solution!

public enum Foo {;
  public static int bar() {
    return 1;
  }
}
3
kan

ClassUnderTest testClass = Whitebox.invokeConstructor (ClassUnderTest.class);

1
acpuma

Je ne connais pas Cobertura, mais j'utilise Clover et il permet d'ajouter des exclusions de correspondance de motif. Par exemple, j'ai des modèles qui excluent les lignes de journalisation Apache-commons-afin qu'elles ne soient pas comptées dans la couverture.

1
John Engelman

Une autre option consiste à créer un initialiseur statique similaire au code suivant.

class YourClass {
  private YourClass() {
  }
  static {
     new YourClass();
  }

  // real ops
}

De cette manière, le constructeur privé est considéré comme testé et les frais généraux d’exécution ne sont en principe pas mesurables. Je fais cela pour obtenir une couverture à 100% en utilisant EclEmma, ​​mais cela fonctionne probablement pour tous les outils de couverture. Bien entendu, l’inconvénient de cette solution est que vous écrivez du code de production (l’initialiseur statique) uniquement à des fins de test.

1
Christian Lewold

Mon choix: utiliser lombok.

Plus précisément, le @UtilityClass annotation . (Malheureusement, seulement "expérimental" au moment de la rédaction, mais cela fonctionne très bien et a une perspective positive, donc susceptible de passer bientôt à stable.)

Cette annotation ajoutera le constructeur privé pour empêcher l’instanciation et rendra la classe finale. Lorsqu'il est combiné avec lombok.addLombokGeneratedAnnotation = true dans lombok.config, à peu près tous les frameworks de test ignorent le code généré automatiquement lors du calcul de la couverture de test, ce qui vous permet de contourner la couverture de ce code généré automatiquement, sans piratage ni réflexion.

1
Michael Berry

Parfois, Cobertura marque le code non destiné à être exécuté comme "non couvert", il n'y a rien de mal à cela. Pourquoi craignez-vous d'avoir 99% couverture au lieu de 100%?

Techniquement, vous pouvez toujours invoquer ce constructeur avec réflexion, mais cela me semble très faux (dans ce cas).

0
Nikita Rybak
@Test
public void testTestPrivateConstructor() {
    Constructor<Test> cnt;
    try {
        cnt = Test.class.getDeclaredConstructor();
        cnt.setAccessible(true);

        cnt.newInstance();
    } catch (Exception e) {
        e.getMessage();
    }
}

Test.Java est votre fichier source, qui contient votre constructeur privé

0
DPREDDY

Si je devais deviner le but de votre question, je dirais:

  1. Vous voulez des contrôles raisonnables pour les constructeurs privés qui travaillent réellement, et
  2. Vous voulez que le trèfle exclue les constructeurs vides pour les classes util.

Pour 1, il est évident que vous souhaitiez que toute l'initialisation soit effectuée via les méthodes d'usine. Dans de tels cas, vos tests devraient pouvoir tester les effets secondaires du constructeur. Cela devrait entrer dans la catégorie des tests de méthodes privées normales. Réduisez la taille des méthodes afin qu'elles ne réalisent qu'un nombre limité de tâches déterminées (idéalement, une seule et même bonne chose), puis testez les méthodes qui les utilisent.

Par exemple, si mon constructeur [privé] configure les champs d'instance de ma classe a à 5. Ensuite, je peux (ou plutôt dois) le tester:

@Test
public void testInit() {
    MyClass myObj = MyClass.newInstance(); //Or whatever factory method you put
    Assert.assertEquals(5, myObj.getA()); //Or if getA() is private then test some other property/method that relies on a being 5
}

Pour 2, vous pouvez configurer Clover pour exclure les constructeurs Util si vous avez un modèle de nommage défini pour les classes Util. Par exemple, dans mon propre projet, j'utilise quelque chose comme ceci (car nous suivons la convention selon laquelle les noms de toutes les classes Util doivent se terminer par Util):

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
</clover-setup>

J'ai délibérément laissé de côté un .* Suivant ) parce que ces constructeurs ne sont pas censés lancer des exceptions (ils ne sont censés faire rien).

Il peut bien sûr y avoir un troisième cas où vous voudrez peut-être avoir un constructeur vide pour une classe non utilitaire. Dans de tels cas, je vous recommanderais de mettre un methodContext avec la signature exacte du constructeur.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
    <methodContext name="myExceptionalClassCtor" regexp="^private MyExceptionalClass()$"/>
</clover-setup>

Si vous avez beaucoup de telles classes exceptionnelles, vous pouvez choisir de modifier le constructeur privé généralisé reg-ex que j'ai suggéré et d'enlever Util. Dans ce cas, vous devrez vous assurer manuellement que les effets secondaires de votre constructeur sont toujours testés et couverts par d'autres méthodes de votre classe/projet.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+ *( *) .*"/>
</clover-setup>
0
Apoorv Khurasia