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


110

Voici le code:

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

Voici 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é?


Il me semble que vous essayez d'appliquer le modèle Singleton. Si c'est le cas, vous pourriez aimer dp4j.com (qui fait exactement cela)
simpatico

ne devrait-il pas remplacer «intentionnellement vide» par une exception de lancement? Dans ce cas, vous pouvez écrire des tests qui attendent cette exception spécifique avec un message spécifique, non? Je ne sais pas si c'est exagéré
Ewoks

Réponses:


85

Eh bien, il existe des moyens d'utiliser la réflexion, etc., mais cela en vaut-il vraiment la peine? C'est un constructeur qui ne devrait jamais être appelé , non?

S'il y a une annotation ou quelque chose de similaire que vous pouvez ajouter à la classe pour faire comprendre à Cobertura qu'elle ne sera pas appelée, faites-le: je ne pense pas que cela 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 simplement avec la couverture légèrement réduite. Rappelez - vous que la couverture est censée être quelque chose qui est utile pour vous - vous devriez être en charge de l'outil, et non l'inverse.


18
Je ne veux pas "réduire légèrement la couverture" dans tout le projet juste à cause de ce constructeur particulier ..
yegor256

36
@Vincenzo: Alors, IMO, vous accordez une valeur trop élevée à un simple nombre. La couverture est un indicateur des tests. 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 zones pour des tests supplémentaires. L'appel artificiel d'un constructeur autrement inutilisé n'aide aucun de ces points.
Jon Skeet

19
@JonSkeet: Je suis totalement d'accord avec "Ne soyez pas esclave d'un outil", mais cela ne sent pas bon de se souvenir de chaque "nombre de défauts" dans chaque projet. Comment s'assurer que le résultat 7/9 est une limitation Cobertura, et non celle du programmeur? Un nouveau programmeur doit saisir chaque échec (ce qui peut être beaucoup dans les grands projets) pour vérifier classe par classe.
Eduardo Costa

5
Cela ne répond pas à la question. et en passant, certains gestionnaires examinent les chiffres de couverture. Ils ne se soucient pas pourquoi. Ils savent que 85% vaut mieux que 75%.
ACV

2
Un cas d'utilisation pratique pour tester du code autrement inaccessible est d'obtenir une couverture de test à 100% afin que personne n'ait à revoir cette classe. Si la couverture est bloquée à 95%, de nombreux développeurs peuvent tenter d'en comprendre la raison pour se heurter à ce problème encore et encore.
thisismydesign

140

Je ne suis pas entièrement 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. Dites à votre outil de couverture d'ignorer le constructeur, ou mettez l'idéalisme de côté et écrivez le test suivant et en avez terminé:

@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();
}

25
Mais cela élimine le bruit dans le rapport de couverture en ajoutant du bruit à la suite de tests. J'aurais juste terminé la phrase par "mettre l'idéalisme de côté". :)
Christopher Orr

11
Pour donner à ce test une quelconque signification, vous devriez probablement également affirmer que le niveau d'accès du constructeur est ce que vous attendez.
Jeremy

En ajoutant la mauvaise réflexion plus les idées de Jeremy plus un nom signifiant comme "testIfConstructorIsPrivateWithoutRaisingExceptions", je suppose que c'est "LA" réponse.
Eduardo Costa

1
Ceci est syntaxiquement incorrect, n'est-ce pas? Qu'est-ce que c'est constructor? Ne devrait pas Constructorêtre paramétré et non pas un type brut?
Adam Parkin

2
C'est faux: constructor.isAccessible()renvoie toujours false, même sur un constructeur public. On devrait utiliser assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen

78

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

/**
 * 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 les exemples dans https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test


11
+1 Non seulement cela résout le problème sans tromper l'outil, mais cela teste complètement les normes de codage de la configuration d'une classe d'utilité. J'ai dû changer le test d'accessibilité pour l'utiliser Modifier.isPrivatecomme isAccessiblec'était le cas truepour les constructeurs privés dans certains cas (se moquer des interférences de la bibliothèque?).
David Harkness

4
Je veux vraiment ajouter ceci à la classe Assert de JUnit, mais je ne veux pas vous attribuer le mérite de votre travail. Je pense que c'est très bien. Ce serait génial d'avoir Assert.utilityClassWellDefined()dans JUnit 4.12+. Avez-vous envisagé une pull request?
Visionary Software Solutions

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

Merci, je réinitialise le drapeau accessible. Peut-être s'agit-il d'un bug sur Sonar lui-même?
Archimedes Trajano

J'ai regardé mon rapport Sonar pour une couverture sur mon plugin batik maven, il semble couvrir correctement. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano

19

J'avais rendu privé le constructeur de ma classe de fonctions utilitaires statiques, pour satisfaire CheckStyle. Mais comme l'affiche originale, j'ai demandé à Cobertura de se plaindre 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 consistent à savoir si le constructeur reste privé - et cela est rendu redondant par le contrôle d'accessibilité lors du test suivant.

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

Je suis allé avec la suggestion de Javid Jamae et j'ai utilisé la réflexion, mais j'ai ajouté des affirmations pour attraper quelqu'un qui joue avec la classe testée (et j'ai nommé le test pour indiquer High Levels Of Evil).

@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 tellement exagéré, mais je dois admettre que j'aime la sensation chaleureuse et floue d'une couverture de méthode à 100%.


C'est peut-être excessif, mais si c'était à Unitils ou similaire, je l'utiliserais
Stewart

+1 Bon début, même si je suis allé avec le test plus complet d'Archimède .
David Harkness

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

IMO, la solution du premier extrait de code est la plus claire et la plus simple de cette discussion. Il fail(...)n'est pas nécessaire de simplement aligner avec .
Piotr Wittchen

9

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

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

package com.XXX;

public interface Foo {

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

Il n'y a aucun constructeur et aucune plainte de Cobertura. Vous devez maintenant tester uniquement les lignes qui vous intéressent vraiment.


1
Malheureusement cependant, vous ne pouvez pas déclarer l'interface comme "finale", empêchant quiconque de la sous-classer - sinon ce serait la meilleure approche.
Michael Berry

5

Le raisonnement derrière le test de code qui ne fait rien est d'atteindre une couverture de code à 100% et de remarquer quand 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'agissait simplement d'un constructeur privé. Au fur et à mesure que votre base de code grandit, vous ressentirez une agréable sensation de chaleur en regardant à 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 dire à l'outil de couverture de code d'ignorer la méthode (peut-être une annotation ou un fichier de configuration) car vous seriez alors 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 qui appartiennent à une classe finale car le constructeur est là comme une mesure de «sécurité» rien d'autre :)
J'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 puis ajoutez simplement des classes au tableau au fur et à mesure.


5

Les nouvelles versions 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 les constructeurs / méthodes qui contiennent une ligne de code. Certains exemples incluent un appel à un super constructeur uniquement, des méthodes getter / setter, etc. Pour inclure l'argument ignorer trivial, ajoutez ce qui suit:

<cobertura-instrument ignoreTrivial="true" />

ou dans une version Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

4

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

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

Vous pouvez également utiliser les annotations de couverture : @CoverageIgnore.


3

Enfin, il y a une solution!

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

Comment est-ce que cela teste la classe qui est publiée dans la question? Vous ne devriez pas supposer que vous pouvez transformer chaque classe avec un constructeur privé en une énumération, ou que vous voudriez.
Jon Skeet

@JonSkeet Je peux pour la classe en question. Et la plupart des classes utilitaires qui n'ont que des méthodes statiques. Sinon, une classe avec le seul constructeur privé n'a aucun sens.
kan

1
Une classe avec un constructeur privé peut être instanciée à partir de méthodes statiques publiques, bien que bien sûr il soit alors facile d'obtenir la couverture. Mais fondamentalement, je préférerais que toute classe qui s'étende Enum<E>soit vraiment une énumération ... Je crois que cela révèle mieux l'intention.
Jon Skeet

4
Wow, je préfère absolument le code qui a du sens sur un nombre assez arbitraire. (La couverture n'est pas une garantie de qualité, et une couverture à 100% n'est pas réalisable dans tous les cas. Vos tests devraient au mieux guider votre code - pas le diriger sur une falaise d'intentions bizarres.)
Jon Skeet

1
@Kan: Ajouter un appel factice au constructeur pour bluffer l'outil ne devrait pas être l'intention. Quiconque s'appuie sur une seule métrique pour déterminer le bien-être du projet est déjà sur la voie de la destruction.
Apoorv Khurasia

1

Je ne sais pas pour Cobertura mais j'utilise Clover et il a un moyen d'ajouter des exclusions de correspondance de motifs. 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

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

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

  // real ops
}

De cette façon, le constructeur privé est considéré comme testé et la surcharge d'exécution n'est fondamentalement pas mesurable. Je fais cela pour obtenir une couverture à 100% en utilisant EclEmma, ​​mais cela fonctionne probablement pour chaque outil de couverture. L'inconvénient de cette solution, bien sûr, est que vous écrivez du code de production (l'initialiseur statique) uniquement à des fins de test.


Je fais ça un peu. Bon marché comme bon marché, bon marché comme sale, mais efficace.
pholser

Avec Sonar, cela fait en fait que la classe est entièrement manquée par la couverture du code.
Adam Parkin

1

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


Cela aurait dû être la bonne réponse car elle répond exactement à ce qui est demandé.
Chakian

0

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

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


0

Si je devais deviner l'intention de votre question, je dirais:

  1. Vous voulez des vérifications raisonnables pour les constructeurs privés qui font un travail réel, et
  2. Vous voulez que Clover exclut les constructeurs vides pour les classes util.

Pour 1, il est évident que vous souhaitez que toutes les initialisations soient effectuées via des 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 fassent qu'un nombre limité de choses déterminées (idéalement, juste une chose et une chose bien), puis testez les méthodes qui reposent sur elles.

Par exemple, si mon constructeur [privé] configure les champs d'instance de ma classe asur 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 dénomination défini pour les classes Util. Par exemple, dans mon propre projet, j'utilise quelque chose comme ceci (parce que 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 de tels constructeurs ne sont pas destinés à lancer des exceptions (ils ne sont pas destinés à faire quoi que ce soit).

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 recommande de mettre un methodContextavec 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 classes exceptionnelles, vous pouvez choisir de modifier le constructeur privé généralisé reg-ex que j'ai suggéré et de le supprimer 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 dans 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
@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 a votre constructeur privé


Ce serait bien d'expliquer pourquoi cette construction aide à la couverture.
Markus

Vrai, et deuxièmement: pourquoi attraper une exception dans votre test? L'exception levée devrait en fait faire échouer le test.
Jordi

0

Ce qui suit m'a fonctionné sur une classe créée avec l'annotation Lombok @UtilityClass, qui ajoute automatiquement un constructeur privé.

@Test
public void testConstructorIsPrivate() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
    Constructor<YOUR_CLASS_NAME> constructor = YOUR_CLASS_NAME.class.getDeclaredConstructor();
    assertTrue(Modifier.isPrivate(constructor.getModifiers())); //this tests that the constructor is private
    constructor.setAccessible(true);
    assertThrows(InvocationTargetException.class, () -> {
        constructor.newInstance();
    }); //this add the full coverage on private constructor
}

Bien que constructor.setAccessible (true) devrait fonctionner lorsque le constructeur privé a été écrit manuellement, avec Lombok, l'annotation ne fonctionne pas, car elle le force. Constructor.newInstance () teste en fait que le constructeur est appelé et cela complète la couverture du costructeur lui-même. Avec l'assertThrows, vous évitez que le test échoue et vous avez géré l'exception car c'est exactement l'erreur que vous attendez. Bien que ce soit une solution de contournement et que je n'apprécie pas le concept de «couverture de ligne» par rapport à «couverture de fonctionnalité / comportement», nous pouvons trouver un sens à ce test. En fait, vous êtes sûr que la classe utilitaire a en fait un constructeur privé qui lève correctement une exception lorsqu'il est appelé également via reflaction. J'espère que cela t'aides.


Salut @ShanteshwarInde. Merci beaucoup. Ma contribution a été modifiée et complétée suite à vos suggestions. Cordialement.
Riccardo Solimena

0

Mon option préférée en 2019: utiliser lombok.

Plus précisément, l' @UtilityClassannotation . (Malheureusement seulement "expérimental" au moment de l'écriture, mais il fonctionne très bien et a une perspective positive, donc susceptible d'être bientôt mis à niveau vers stable.)

Cette annotation ajoutera le constructeur privé pour empêcher l'instanciation et rendra la classe finale. Lorsqu'ils sont combinés avec lombok.addLombokGeneratedAnnotation = truein lombok.config, pratiquement tous les frameworks de test ignoreront le code généré automatiquement lors du calcul de la couverture de test, vous permettant de contourner la couverture de ce code généré automatiquement sans piratage ni réflexion.


-2

Vous ne pouvez pas.

Vous créez apparemment le constructeur privé pour empêcher l'instanciation d'une classe destinée à ne contenir que des méthodes statiques. Plutôt que d'essayer d'obtenir une couverture de ce constructeur (ce qui exigerait que la classe soit instanciée), vous devriez vous en débarrasser et faire confiance à vos développeurs pour ne pas ajouter de méthodes d'instance à la classe.


3
C'est incorrect; vous pouvez l'instancier par réflexion, comme indiqué ci-dessus.
theotherian

C'est mauvais, ne laissez jamais le constructeur public par défaut apparaître, vous devriez ajouter le constructeur privé pour éviter de l'appeler.
Lho Ben
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.