Come aggiungere una copertura di test a un costruttore privato?


110

Questo è il codice:

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

Questo è il 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
  }
}

Funziona bene, la classe è testata. Ma Cobertura dice che non c'è copertura del codice del costruttore privato della classe. Come possiamo aggiungere una copertura di test a un costruttore così privato?


Mi sembra che tu stia cercando di far rispettare il modello Singleton. Se è così, ti potrebbe piacere dp4j.com (che fa esattamente questo)
simpatico

"intenzionalmente vuoto" non dovrebbe essere sostituito con il lancio di un'eccezione? In quel caso potresti scrivere test che si aspettano quella specifica eccezione con un messaggio specifico, no? non sono sicuro che sia eccessivo
Ewoks

Risposte:


85

Bene, ci sono modi in cui potresti potenzialmente usare la riflessione, ecc. - Ma ne vale davvero la pena? Questo è un costruttore che non dovrebbe mai essere chiamato , giusto?

Se c'è un'annotazione o qualcosa di simile che puoi aggiungere alla classe per far capire a Cobertura che non verrà chiamata, fallo: non penso che valga la pena fare i cerchi per aggiungere una copertura artificialmente.

EDIT: Se non c'è modo di farlo, vivi con una copertura leggermente ridotta. Ricorda che la copertura deve essere qualcosa di utile per te : dovresti essere responsabile dello strumento, non il contrario.


18
Non voglio "ridurre leggermente la copertura" nell'intero progetto solo a causa di questo particolare costruttore ..
yegor256

36
@Vincenzo: Allora IMO stai mettendo un valore troppo alto su un numero semplice. La copertura è un indicatore dei test. Non essere schiavo di uno strumento. Il punto di copertura è darti un livello di fiducia e suggerire aree per ulteriori test. Chiamare artificialmente un costruttore altrimenti inutilizzato non aiuta con nessuno di questi punti.
Jon Skeet

19
@ JonSkeet: Sono totalmente d'accordo con "Non essere schiavo di uno strumento", ma non ha un buon profumo ricordare ogni "conteggio dei difetti" in ogni progetto. Come assicurarsi che il risultato 7/9 sia una limitazione di Cobertura e non del programmatore? Un nuovo programmatore deve inserire ogni errore (che può essere molto in progetti di grandi dimensioni) per controllare classe per classe.
Eduardo Costa

5
Questo non risponde alla domanda. e, a proposito, alcuni manager guardano i numeri di copertura. A loro non importa il motivo. Sanno che l'85% è migliore del 75%.
ACV

2
Un caso d'uso pratico per testare codice altrimenti inaccessibile è ottenere una copertura del test del 100% in modo che nessuno debba guardare di nuovo quella classe. Se la copertura è bloccata al 95%, molti sviluppatori potrebbero tentare di capirne il motivo solo per imbattersi in questo problema più e più volte.
thisismydesign

140

Non sono del tutto d'accordo con Jon Skeet. Penso che se puoi ottenere una vittoria facile per darti copertura ed eliminare il rumore nel tuo rapporto sulla copertura, allora dovresti farlo. Dì al tuo strumento di copertura di ignorare il costruttore, o metti da parte l'idealismo e scrivi il seguente test e finisci con esso:

@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
Ma questo sta eliminando il rumore nel rapporto di copertura aggiungendo rumore alla suite di test. Avrei appena concluso la frase con "metti da parte l'idealismo". :)
Christopher Orr

11
Per dare a questo test qualsiasi tipo di significato, dovresti probabilmente anche affermare che il livello di accesso del costruttore è quello che ti aspetti che sia.
Jeremy

Aggiungendo il riflesso malvagio più le idee di Jeremy più un nome significativo come "testIfConstructorIsPrivateWithoutRaisingExceptions", immagino che questa sia "LA" risposta.
Eduardo Costa

1
Questo è sintatticamente errato, non è vero? Che cos'è constructor? Non dovrebbe Constructoressere parametrizzato e non un tipo grezzo?
Adam Parkin

2
Questo è sbagliato: constructor.isAccessible()restituisce sempre false, anche su un costruttore pubblico. Uno dovrebbe usare assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen

78

Sebbene non sia necessariamente per la copertura, ho creato questo metodo per verificare che la classe di utilità sia ben definita e anche per fare un po 'di copertura.

/**
 * 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);
        }
    }
}

Ho inserito il codice completo e gli esempi in https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test


11
+1 Non solo questo risolve il problema senza ingannare lo strumento, ma verifica completamente gli standard di codifica per la creazione di una classe di utilità. Ho dovuto modificare il test di accessibilità da utilizzare Modifier.isPrivatecome in alcuni casi isAccessibleveniva restituito trueper i costruttori privati ​​(deridendo l'interferenza della libreria?).
David Harkness

4
Voglio davvero aggiungerlo alla classe Assert di JUnit, ma non voglio prendermi il merito del tuo lavoro. Penso che sia molto buono. Sarebbe fantastico avere Assert.utilityClassWellDefined()in JUnit 4.12+. Hai considerato una richiesta pull?
Soluzioni software visionarie

Si noti che l'utilizzo setAccessible()per rendere accessibile il costruttore causa problemi allo strumento di copertura del codice di Sonar (quando lo faccio la classe scompare dai rapporti di copertura del codice di Sonar).
Adam Parkin

Grazie, resetto comunque il flag accessibile. Forse è un bug su Sonar stesso?
Archimedes Trajano

Ho guardato il mio report Sonar per la copertura sul mio plugin batik maven, sembra coprire correttamente. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano

19

Avevo reso privato il costruttore della mia classe di funzioni di utilità statiche, per soddisfare CheckStyle. Ma come il poster originale, avevo Cobertura che si lamentava del test. All'inizio ho provato questo approccio, ma ciò non influisce sul report di copertura perché il costruttore non viene mai effettivamente eseguito. Quindi, in realtà, tutto questo test è se il costruttore rimane privato - e questo è reso ridondante dal controllo dell'accessibilità nel test successivo.

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

Sono andato con il suggerimento di Javid Jamae e ho usato la riflessione, ma ho aggiunto affermazioni per catturare qualcuno che scherzava con la classe in fase di test (e ho chiamato il test per indicare 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());
}

Questo è così eccessivo, ma devo ammettere che mi piace la calda sensazione sfocata della copertura del metodo al 100%.


Può essere eccessivo, ma se fosse in Unitils o simili, lo userei
Stewart

+1 Buon inizio, anche se sono andato con il test più completo di Archimede .
David Harkness,

Il primo esempio non funziona: IllegalAccesException significa che il costruttore non viene mai chiamato, quindi la copertura non viene registrata.
Tom McIntyre

IMO, la soluzione nel primo frammento di codice è la più pulita e la più semplice in questa discussione. Solo la linea con fail(...)non è necessaria.
Piotr Wittchen

9

Con Java 8 è possibile trovare altre soluzioni.

Presumo che tu voglia semplicemente creare una classe di utilità con pochi metodi statici pubblici. Se puoi usare Java 8, puoi usare interfaceinvece.

package com.XXX;

public interface Foo {

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

Non c'è nessun costruttore e nessun reclamo da Cobertura. Ora devi testare solo le linee che ti interessano veramente.


1
Sfortunatamente, tuttavia, non puoi dichiarare l'interfaccia come "finale", impedendo a chiunque di sottoclassarla - altrimenti questo sarebbe l'approccio migliore.
Michael Berry

5

Il ragionamento alla base del test del codice che non fa nulla è ottenere una copertura del codice del 100% e notare quando la copertura del codice diminuisce. Altrimenti si potrebbe sempre pensare, ehi, non ho più una copertura del codice al 100% ma è PROBABILMENTE a causa dei miei costruttori privati. Ciò semplifica l'individuazione di metodi non testati senza dover verificare che si tratti solo di un costruttore privato. Man mano che la base del codice cresce, sentirai davvero una bella sensazione di calore guardando il 100% invece del 99%.

IMO è meglio usare la riflessione qui poiché altrimenti dovresti ottenere uno strumento di copertura del codice migliore che ignori questi costruttori o in qualche modo dire allo strumento di copertura del codice di ignorare il metodo (forse un'annotazione o un file di configurazione) perché allora saresti bloccato con uno specifico strumento di copertura del codice.

In un mondo perfetto tutti gli strumenti di copertura del codice ignorerebbero i costruttori privati ​​che appartengono a una classe finale perché il costruttore è lì come misura di "sicurezza" nient'altro :)
Io userei questo codice:

    @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());
        }
    }
E poi aggiungi semplicemente le classi all'array mentre procedi.


5

Le versioni più recenti di Cobertura hanno il supporto integrato per ignorare banali getter / setter / costruttori:

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

Ignora banale

Ignora banale consente la possibilità di escludere costruttori / metodi che contengono una riga di codice. Alcuni esempi includono solo una chiamata a un supercostruttore, metodi getter / setter, ecc. Per includere l'argomento ignora banale aggiungere quanto segue:

<cobertura-instrument ignoreTrivial="true" />

o in una build Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

4

Non farlo. Qual è il punto nel testare un costruttore vuoto? Poiché cobertura 2.0 esiste un'opzione per ignorare questi casi banali (insieme a setter / getter), puoi abilitarlo in maven aggiungendo la sezione di configurazione al plugin cobertura maven:

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

In alternativa è possibile utilizzare copertura Annotazioni : @CoverageIgnore.


3

Finalmente c'è la soluzione!

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

Ma come sta testando la classe pubblicata nella domanda? Non dovresti presumere di poter trasformare ogni classe con un costruttore privato in un'enumerazione, o che vorresti farlo.
Jon Skeet

@ JonSkeet posso per la classe in questione. E la maggior parte delle classi di utilità che hanno solo un mucchio di metodi statici. Altrimenti una classe con l'unico costruttore privato non ha alcun senso.
kan

1
Una classe con un costruttore privato può essere istanziata da metodi statici pubblici, anche se ovviamente è facile ottenere la copertura. Ma fondamentalmente preferirei qualsiasi classe che si estenda Enum<E>per essere davvero un enum ... credo che riveli meglio l'intento.
Jon Skeet

4
Wow, preferirei assolutamente un codice che abbia senso rispetto a un numero piuttosto arbitrario. (La copertura non è una garanzia di qualità, né la copertura del 100% è fattibile in tutti i casi. I tuoi test dovrebbero guidare il tuo codice al meglio, non guidarlo oltre un precipizio di intenzioni bizzarre.)
Jon Skeet

1
@Kan: Aggiungere una chiamata fittizia al costruttore per bluffare lo strumento non dovrebbe essere l'intento. Chiunque si affidi a una singola metrica per determinare il benessere del progetto è già sulla via della distruzione.
Apoorv Khurasia

1

Non so Cobertura ma uso Clover e ha un mezzo per aggiungere esclusioni di corrispondenza dei modelli. Ad esempio, ho modelli che escludono le righe di registrazione di apache-commons in modo che non vengano conteggiati nella copertura.


1

Un'altra opzione è creare un inizializzatore statico simile al codice seguente

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

  // real ops
}

In questo modo il costruttore privato viene considerato testato e il sovraccarico di runtime non è fondamentalmente misurabile. Lo faccio per ottenere una copertura del 100% utilizzando EclEmma, ​​ma probabilmente funziona per ogni strumento di copertura. Lo svantaggio di questa soluzione, ovviamente, è che scrivi codice di produzione (l'inizializzatore statico) solo a scopo di test.


Lo faccio un bel po '. Economico come poco costoso, economico come sporco, ma efficace.
Pholser

Con Sonar, questo fa sì che la classe venga persa completamente dalla copertura del codice.
Adam Parkin

1

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


Questa avrebbe dovuto essere la risposta corretta in quanto risponde esattamente a ciò che viene chiesto.
Chakian

0

A volte Cobertura contrassegna il codice non destinato ad essere eseguito come "non coperto", non c'è niente di sbagliato in questo. Perché ti preoccupi di avere una 99%copertura invece di 100%?

Tecnicamente, però, puoi ancora invocare quel costruttore con la riflessione, ma mi suona molto sbagliato (in questo caso).


0

Se dovessi indovinare l'intento della tua domanda direi:

  1. Desideri controlli ragionevoli per i costruttori privati ​​che svolgono un lavoro effettivo e
  2. Vuoi che clover escluda i costruttori vuoti per le classi util.

Per 1, è ovvio che si desidera che tutta l'inizializzazione venga eseguita tramite metodi di fabbrica. In questi casi, i tuoi test dovrebbero essere in grado di testare gli effetti collaterali del costruttore. Questo dovrebbe rientrare nella categoria dei normali test con metodo privato. Rendi i metodi più piccoli in modo che eseguano solo un numero limitato di cose determinate (idealmente, solo una cosa e una cosa bene) e quindi testare i metodi che si basano su di essi.

Ad esempio, se il mio costruttore [privato] imposta i campi di istanza della mia classe asu 5. Quindi posso (o meglio devo) testarlo:

@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
}

Per 2, puoi configurare clover per escludere i costruttori Util se hai un modello di denominazione impostato per le classi Util. Ad esempio, nel mio progetto uso qualcosa di simile (perché seguiamo la convenzione secondo la quale i nomi per tutte le classi Util dovrebbero terminare con Util):

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

Ho deliberatamente tralasciato un .*seguito )perché tali costruttori non hanno lo scopo di generare eccezioni (non hanno lo scopo di fare nulla).

Ovviamente può esserci un terzo caso in cui potresti voler avere un costruttore vuoto per una classe non di utilità. In questi casi, ti consiglio di inserire una methodContextcon la firma esatta del costruttore.

<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>

Se hai molte di queste classi eccezionali, puoi scegliere di modificare il costruttore privato generalizzato reg-ex che ho suggerito e rimuoverlo Util. In questo caso, dovrai assicurarti manualmente che gli effetti collaterali del tuo costruttore siano ancora testati e coperti da altri metodi nella tua classe / progetto.

<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 è il tuo file sorgente, che ha il tuo costruttore privato


Sarebbe bello spiegare perché questo costrutto aiuta con la copertura.
Markus

Vero, e in secondo luogo: perché cogliere un'eccezione nel test? L'eccezione generata dovrebbe effettivamente far fallire il test.
Jordi

0

Quanto segue ha funzionato per me su una classe creata con l'annotazione Lombok @UtilityClass, che aggiunge automaticamente un costruttore privato.

@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
}

Sebbene constructor.setAccessible (true) dovrebbe funzionare quando il costruttore privato è stato scritto manualmente, con l'annotazione Lombok non funziona, poiché lo forza. Constructor.newInstance () verifica effettivamente che il costruttore sia invocato e questo completa la copertura del costruttore stesso. Con assertThrows eviti che il test fallisca e hai gestito l'eccezione poiché è esattamente l'errore che ti aspetti. Sebbene questa sia una soluzione alternativa e non apprezzo il concetto di "copertura di linea" rispetto a "copertura di funzionalità / comportamento", possiamo trovare un senso in questo test. Infatti sei sicuro che la Utility Class abbia effettivamente un Constructor privato che lancia correttamente un'eccezione quando invocato anche tramite reflaction. Spero che questo ti aiuti.


Ciao @ShanteshwarInde. Molte grazie. Il mio input è stato modificato e completato seguendo i tuoi suggerimenti. Saluti.
Riccardo Solimena

0

La mia opzione preferita nel 2019: usa lombok.

Nello specifico, l' @UtilityClassannotazione . (Purtroppo solo "sperimentale" al momento della scrittura, ma funziona perfettamente e ha una prospettiva positiva, quindi è probabile che presto verrà aggiornato a stabile.)

Questa annotazione aggiungerà il costruttore privato per impedire la creazione di istanze e renderà la classe finale. Se combinato con lombok.addLombokGeneratedAnnotation = truein lombok.config, praticamente tutti i framework di test ignoreranno il codice generato automaticamente durante il calcolo della copertura del test, consentendo di aggirare la copertura di quel codice generato automaticamente senza hack o riflessioni.


-2

Non puoi.

Apparentemente stai creando il costruttore privato per impedire la creazione di istanze di una classe che dovrebbe contenere solo metodi statici. Piuttosto che cercare di ottenere la copertura di questo costruttore (il che richiederebbe l'istanza della classe), dovresti sbarazzartene e fidarti dei tuoi sviluppatori per non aggiungere metodi di istanza alla classe.


3
Non è corretto; puoi istanziarlo attraverso la riflessione, come notato sopra.
theotherian

Non va bene, non lasciare mai che il costruttore pubblico predefinito venga visualizzato, dovresti aggiungere quello privato per evitare di chiamarlo.
Lho Ben
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.