121 votes

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

Ceci est le code :

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

Ceci 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 ici
  }
}

Fonctionne bien, la classe est testée. Mais Cobertura indique 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é ?

0 votes

Il me semble que vous essayez d'appliquer le pattern Singleton. Si c'est le cas, vous pourriez aimer dp4j.com (qui fait exactement ça)

0 votes

Ne devrait-il pas être remplacé par la levée d'une exception intentionnellement vide? Dans ce cas, vous pourriez écrire un test qui s'attend à cette exception spécifique avec un message spécifique, non? pas sûr si c'est exagéré

151voto

Javid Jamae Points 2971

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

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

26 votes

Mais cela élimine le bruit dans le rapport de couverture en ajoutant du bruit au banc d'essai. J'aurais simplement terminé la phrase par "mettre l'idéalisme de côté". :)

11 votes

Pour donner un sens à ce test, vous devriez probablement également affirmer que le niveau d'accès du constructeur est celui auquel vous vous attendez.

0 votes

En ajoutant la réflexion maléfique plus les idées de Jeremy plus un nom significatif comme "testIfConstructorIsPrivateWithoutRaisingExceptions", je suppose que c'est "LA" réponse.

89voto

Jon Skeet Points 692016

Eh bien, il existe des moyens que vous pourriez potentiellement utiliser la réflexion, etc. - mais en vaut-il vraiment la peine? C'est un constructeur qui ne devrait jamais être appelé, 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'il ne sera pas appelé, faites-le : je ne pense pas que cela vaille la peine de se compliquer la vie pour ajouter artificiellement une couverture.

MODIFIER : S'il n'y a aucun moyen de le faire, contentez-vous d'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 maître de l'outil, pas l'inverse.

0 votes

@Vincenzo : Eh bien, cela exclut toute la classe - vous voulez juste exclure le constructeur.

18 votes

Je ne veux pas "réduire légèrement la couverture" dans l'ensemble du projet juste à cause de ce constructeur particulier.

38 votes

@Vincenzo: Alors, à mon avis, vous accordez une trop grande valeur à un simple chiffre. La couverture est un indicateur de test. Ne soyez pas esclave d'un outil. Le but de la couverture est de vous donner un niveau de confiance, et de suggérer des domaines pour des tests supplémentaires. Appeler artificiellement un constructeur autrement inutilisé ne contribue pas à atteindre ces objectifs.

81voto

Archimedes Trajano Points 2729

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

/**
 * Vérifie qu'une classe utilitaire est bien définie.
 * 
 * @param clazz
 *            classe utilitaire à vérifier.
 */
public static void assertUtilityClassWellDefined(final Class clazz)
        throws NoSuchMethodException, InvocationTargetException,
        InstantiationException, IllegalAccessException {
    Assert.assertTrue("la classe doit être finale",
            Modifier.isFinal(clazz.getModifiers()));
    Assert.assertEquals("Il doit y avoir un seul constructeur", 1,
            clazz.getDeclaredConstructors().length);
    final Constructor constructor = clazz.getDeclaredConstructor();
    if (constructor.isAccessible() || 
                !Modifier.isPrivate(constructor.getModifiers())) {
        Assert.fail("le constructeur n'est pas privé");
    }
    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("il existe une méthode non statique:" + method);
        }
    }
}

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

12 votes

+1 Non seulement cela résout le problème sans tromper l'outil, mais cela teste pleinement les normes de codage pour la mise en place d'une classe utilitaire. J'ai dû modifier le test d'accessibilité pour utiliser Modifier.isPrivate car isAccessible retournait true pour les constructeurs privés dans certains cas (interférence de la bibliothèque de simulation ?).

4 votes

Je veux vraiment ajouter cela à la classe Assert de JUnit, mais je ne veux pas prendre crédit pour votre travail. Je pense que c'est très bon. Ce serait formidable d'avoir Assert.utilityClassWellDefined() dans JUnit 4.12+. Avez-vous envisagé une demande de tirage (pull request) ?

0 votes

Notez que l'utilisation de setAccessible() pour rendre le constructeur accessible pose des problèmes pour l'outil de couverture de code de Sonar (lorsque je le fais, la classe disparaît des rapports de couverture de code de Sonar).

21voto

Ben Hardy Points 951

J'avais rendu privé le constructeur de ma classe de fonctions utilitaires statiques, pour satisfaire CheckStyle. Mais comme l'a dit l'auteur original, j'avais Cobertura qui se plaignait 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 tout ce que ces tests font vraiment, c'est vérifier si le constructeur reste privé - ce qui est redondant avec la vérification d'accessibilité dans le test suivant.

@Test(expected=IllegalAccessException.class)
public void testConstructorPrivate() throws Exception {
    MyUtilityClass.class.newInstance();
    fail("Le constructeur de la classe utilitaire doit être privé");
}

J'ai suivi la suggestion de Javid Jamae et utilisé la réflexion, mais j'ai ajouté des assertions pour attraper quiconque manipulerait la classe testée (et j'ai nommé le test pour indiquer des Niveaux Élevés de Mal).

@Test
public void evilConstructorInaccessibilityTest() throws Exception {
    Constructor[] ctors = MyUtilityClass.class.getDeclaredConstructors();
    assertEquals("La classe utilitaire ne devrait avoir qu'un seul constructeur",
            1, ctors.length);
    Constructor ctor = ctors[0];
    assertFalse("Le constructeur de la classe utilitaire devrait être inaccessible", 
            ctor.isAccessible());
    ctor.setAccessible(true); // évidemment, nous ne ferions jamais cela en production
    assertEquals("Vous vous attendez à ce que le constructeur retourne le type attendu",
            MyUtilityClass.class, ctor.newInstance().getClass());
}

C'est un peu exagéré, mais je dois admettre que j'aime la sensation chaleureuse et réconfortante d'une couverture à 100% des méthodes.

0 votes

Il peut sembler excessif, mais si c'était inclus dans Unitils ou quelque chose de similaire, je l'utiliserais

0 votes

+1 Bon début, même si j'ai opté pour le test plus complet d'Archimède.

0 votes

Le premier exemple ne fonctionne pas - l'IllegalAccesException signifie que le constructeur n'est jamais appelé, donc la couverture n'est pas enregistrée.

5voto

jontejj Points 725

La raison pour tester du code qui ne fait rien est d'atteindre une couverture de code de 100% et de remarquer quand la couverture de code diminue. Sinon, on pourrait toujours penser, hey je n'ai plus une couverture de code de 100% mais c'est PROBABLEMENT à cause de mes constructeurs privés. Cela rend facile de repérer les méthodes non testées sans avoir à vérifier que c'était juste un constructeur privé. En grandissant, votre base de code vous donnera réellement une sensation agréable en regardant 100% au lieu de 99%.

À mon avis, il est préférable d'utiliser la réflexion ici car sinon vous devriez soit obtenir un outil de couverture de code meilleur qui ignore ces constructeurs, soit d'une manière ou d'une autre dire à l'outil de couverture de code d'ignorer la méthode (peut-être une Annotation ou un fichier de configuration) car sinon vous seriez coincé 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 qui appartiennent à une classe finale car le constructeur est là comme une mesure "de sécurité" rien d'autre :)
Je utiliserais 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());
        }
    }

Et ajoutez simplement des classes au tableau au fur et à mesure.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X