Jak dodać pokrycie testów do prywatnego konstruktora?


110

To jest kod:

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

To jest 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
  }
}

Działa dobrze, klasa jest przetestowana. Ale Cobertura twierdzi, że prywatny konstruktor klasy ma zerowe pokrycie kodem. Jak możemy dodać pokrycie testów do takiego prywatnego konstruktora?


Wydaje mi się, że próbujesz narzucić wzorzec Singletona. Jeśli tak, możesz polubić dp4j.com (który dokładnie to robi)
simpatico

czy „celowo puste” nie powinno być zastępowane wyrzucaniem wyjątku? W takim przypadku możesz napisać test, który oczekuje tego konkretnego wyjątku z konkretnym komunikatem, nie? nie jestem pewien, czy to przesada
Ewoks

Odpowiedzi:


85

Cóż, są sposoby, aby potencjalnie wykorzystać refleksję itp. - ale czy naprawdę warto? To jest konstruktor, którego nigdy nie należy wywoływać , prawda?

Jeśli istnieje adnotacja lub coś podobnego, które możesz dodać do klasy, aby Cobertura zrozumiał, że nie zostanie wywołana, zrób to: nie sądzę, aby warto było przechodzić przez obręcze, aby sztucznie dodawać pokrycie.

EDYCJA: Jeśli nie ma sposobu, aby to zrobić, po prostu żyj z nieco zmniejszonym zasięgiem. Pamiętaj, że zasięg ma być dla Ciebie przydatny - to ty powinieneś zarządzać narzędziem, a nie odwrotnie.


18
Nie chcę "nieznacznie zmniejszać zasięgu" w całym projekcie tylko z powodu tego konkretnego konstruktora ..
yegor256

36
@Vincenzo: W takim razie IMO ustawiasz zbyt wysoką wartość na prostej liczbie. Pokrycie jest wskaźnikiem testowania. Nie bądź niewolnikiem narzędzia. Chodzi o to, aby dać ci pewien poziom pewności i zasugerować obszary do dodatkowych testów. Sztuczne wywołanie nieużywanego w innym przypadku konstruktora nie pomaga w żadnym z tych punktów.
Jon Skeet

19
@JonSkeet: Całkowicie się zgadzam z „Nie bądź niewolnikiem narzędzia”, ale nie pachnie dobrze zapamiętywać każdą „liczbę błędów” w każdym projekcie. Jak upewnić się, że wynik 7/9 to ograniczenie Cobertury, a nie programisty? Nowy programista musi wpisywać każdą awarię (której może być dużo w dużych projektach), aby sprawdzać klasę po klasie.
Eduardo Costa,

5
To nie odpowiada na pytanie. a tak przy okazji, niektórzy menedżerowie patrzą na numery zasięgu. Nie obchodzi ich dlaczego. Wiedzą, że 85% to lepsze niż 75%.
ACV,

2
Praktycznym przypadkiem użycia do testowania niedostępnego w innym przypadku kodu jest osiągnięcie 100% pokrycia testami, tak aby nikt nie musiał ponownie patrzeć na tę klasę. Jeśli zasięg utknie na poziomie 95%, wielu programistów może próbować znaleźć przyczynę, aby raz po raz napotkać ten problem.
thisismydesign

140

Nie do końca zgadzam się z Jonem Skeetem. Myślę, że jeśli możesz łatwo wygrać, aby zapewnić pokrycie i wyeliminować szum w raporcie pokrycia, powinieneś to zrobić. Albo powiedz swojemu narzędziu pokrycia, aby zignorowało konstruktora, lub odłóż na bok idealizm i napisz następujący test i zakończ z nim:

@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
Ale to eliminuje szum w raporcie pokrycia poprzez dodanie szumu do zestawu testów. Skończyłbym po prostu zdanie „odłóż na bok idealizm”. :)
Christopher Orr

11
Aby nadać temu testowi jakiekolwiek znaczenie, prawdopodobnie powinieneś również zapewnić, że poziom dostępu konstruktora jest taki, jakiego oczekujesz.
Jeremy

Dodając złowrogą refleksję, pomysły Jeremy'ego oraz wymowną nazwę, taką jak „testIfConstructorIsPrivateWithoutRaisingExceptions”, wydaje mi się, że to jest odpowiedź „THE”.
Eduardo Costa,

1
Jest to niepoprawne składniowo, prawda? Co to jest constructor? Nie powinno Constructorbyć parametryzowane i nie powinno być typu surowego?
Adam Parkin

2
To jest złe: constructor.isAccessible()zawsze zwraca false, nawet w publicznym konstruktorze. Należy użyć assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen

78

Chociaż niekoniecznie jest to dla pokrycia, stworzyłem tę metodę, aby sprawdzić, czy klasa narzędzi jest dobrze zdefiniowana, a także zrobić trochę pokrycia.

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

Umieściłem pełny kod i przykłady w https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test


11
+1 Nie tylko rozwiązuje to problem bez oszukiwania narzędzia, ale w pełni testuje standardy kodowania przy tworzeniu klasy użytkowej. Musiałem zmienić test dostępności, aby był używany, Modifier.isPrivateponieważ w niektórych przypadkach wracałem do prywatnych konstruktorów (mockowanie zakłóceń biblioteki?). isAccessibletrue
David Harkness

4
Naprawdę chcę to dodać do klasy Assert w JUnit, ale nie chcę brać pod uwagę twojej pracy. Myślę, że to bardzo dobre. Byłoby wspaniale mieć Assert.utilityClassWellDefined()w JUnit 4.12+. Czy rozważałeś żądanie ściągnięcia?
Visionary Software Solutions

Zauważ, że użycie narzędzia setAccessible()do udostępnienia konstruktora powoduje problemy dla narzędzia pokrycia kodu Sonara (kiedy to zrobię, klasa znika z raportów pokrycia kodu Sonara).
Adam Parkin,

Dzięki, ale resetuję dostępną flagę. Może to błąd samego Sonara?
Archimedes Trajano

Spojrzałem na mój raport Sonar pod kątem pokrycia na mojej wtyczce batik maven, wydaje się, że obejmuje poprawnie. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano

19

Uczyniłem konstruktora mojej klasy statycznych funkcji narzędziowych jako prywatny, aby spełnić wymagania CheckStyle. Ale podobnie jak w przypadku oryginalnego plakatu, Cobertura narzekała na test. Na początku wypróbowałem to podejście, ale nie ma to wpływu na raport pokrycia, ponieważ konstruktor nigdy nie jest wykonywany. Tak naprawdę wszystkie te testy dotyczą sytuacji, w których konstruktor pozostaje prywatny - i staje się to zbędne przez sprawdzenie dostępności w kolejnym teście.

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

Poszedłem z sugestią Javida Jamae i zastosowałem refleksję, ale dodałem asercje, aby złapać każdego, kto majstrował przy testowanej klasie (i nazwałam test, aby wskazać wysoki poziom zła).

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

To przesada, ale muszę przyznać, że podoba mi się ciepłe, rozmyte uczucie 100% pokrycia metodą.


Może to przesada, ale gdyby było w Unitils lub podobnym, użyłbym go
Stewart

+1 Dobry początek, chociaż poszedłem z bardziej kompletnym testem Archimedesa .
David Harkness

Pierwszy przykład nie działa - IllegalAccesException oznacza, że ​​konstruktor nie jest nigdy wywoływany, więc pokrycie nie jest rejestrowane.
Tom McIntyre,

IMO, rozwiązanie w pierwszym fragmencie kodu jest najczystszym i najprostszym rozwiązaniem w tej dyskusji. Po prostu linia z fail(...)nie jest konieczna.
Piotr Wittchen

9

Dzięki Java 8 można znaleźć inne rozwiązanie.

Zakładam, że chcesz po prostu utworzyć klasę użytkową za pomocą kilku publicznych metod statycznych. Jeśli możesz użyć Java 8, możesz interfacezamiast tego użyć .

package com.XXX;

public interface Foo {

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

Nie ma konstruktora i nie ma narzekań ze strony Cobertury. Teraz musisz przetestować tylko te linie, na których naprawdę Ci zależy.


1
Niestety, nie możesz jednak zadeklarować interfejsu jako „ostatecznego”, uniemożliwiając komukolwiek tworzenie podklas - w przeciwnym razie byłoby to najlepsze podejście.
Michael Berry

5

Powodem testowania kodu, który nic nie robi, jest osiągnięcie 100% pokrycia kodu i zauważenie, kiedy spada pokrycie kodu. W przeciwnym razie zawsze można by pomyśleć, hej, nie mam już 100% pokrycia kodu, ale PRAWDOPODOBNIE jest to spowodowane moimi prywatnymi konstruktorami. Ułatwia to wykrycie niesprawdzonych metod bez konieczności sprawdzania, czy był to tylko prywatny konstruktor. Wraz ze wzrostem bazy kodu poczujesz przyjemne, ciepłe uczucie patrząc na 100% zamiast 99%.

IMO najlepiej użyć tutaj odbicia, ponieważ w przeciwnym razie musiałbyś albo uzyskać lepsze narzędzie do pokrycia kodu, które ignoruje te konstruktory, albo w jakiś sposób powiedzieć narzędziu do pokrycia kodu, aby zignorowało metodę (może to być adnotacja lub plik konfiguracyjny), ponieważ wtedy utkniesz za pomocą specjalnego narzędzia pokrycia kodu.

W idealnym świecie wszystkie narzędzia do pokrycia kodu ignorowałyby prywatne konstruktory należące do ostatniej klasy, ponieważ konstruktor jest tam jako środek "bezpieczeństwa", nic innego :)
Użyłbym tego kodu:

    @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());
        }
    }
A potem po prostu dodaj klasy do tablicy w trakcie.


5

Nowsze wersje Cobertury mają wbudowaną obsługę ignorowania trywialnych programów pobierających / ustawiających / konstruktorów:

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

Ignoruj ​​Trivial

Ignoruj ​​trywialne umożliwia wykluczenie konstruktorów / metod, które zawierają jedną linię kodu. Niektóre przykłady obejmują wywołanie tylko superkonstruktora, metody pobierające / ustawiające itp. Aby dołączyć argument ignorowania trywialny, dodaj następujące elementy:

<cobertura-instrument ignoreTrivial="true" />

lub w kompilacji Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

4

Nie. Jaki jest sens testowania pustego konstruktora? Ponieważ cobertura 2.0 istnieje możliwość zignorowania takich trywialnych przypadków (razem z ustawiającymi / pobierającymi), możesz włączyć ją w maven, dodając sekcję konfiguracyjną do wtyczki cobertura maven:

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

Alternatywnie można użyć Coverage adnotacji : @CoverageIgnore.


3

Wreszcie jest rozwiązanie!

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

Jak to jest jednak testować klasę, o której mowa w pytaniu? Nie powinieneś zakładać, że możesz zamienić każdą klasę z prywatnym konstruktorem na wyliczenie lub że chcesz.
Jon Skeet

@JonSkeet Mogę dla danej klasy. I większość klas narzędziowych, które mają tylko kilka metod statycznych. W przeciwnym razie klasa z jedynym prywatnym konstruktorem nie ma sensu.
kan

1
Klasa z prywatnym konstruktorem może zostać utworzona z publicznych metod statycznych, chociaż oczywiście wtedy łatwo jest uzyskać pokrycie. Ale zasadniczo wolałbym każdą klasę, która rozciąga Enum<E>się naprawdę na wyliczenie ... Uważam, że lepiej ujawnia zamiar.
Jon Skeet

4
Wow, zdecydowanie wolałbym kod, który ma sens od dość dowolnej liczby. (Pokrycie nie jest gwarancją jakości, ani nie jest w 100% pokrycie wykonalne we wszystkich przypadkach Twoje testy powinny. Poprowadzić swój kod w najlepszym wypadku. - nie kierować go na urwisku dziwacznych intencji)
Jon Skeet

1
@Kan: Dodanie fałszywego wywołania do konstruktora w celu blefowania narzędzia nie powinno być intencją. Każdy, kto opiera się na jednym mierniku, aby określić stan projektu, jest już na drodze do zniszczenia.
Apoorv Khurasia

1

Nie wiem o Coberturze, ale używam Clover i umożliwia ona dodawanie wykluczeń dopasowywania wzorców. Na przykład mam wzorce, które wykluczają wiersze logowania apache-commons, więc nie są uwzględniane w pokryciu.


1

Inną opcją jest utworzenie statycznego inicjatora podobnego do poniższego kodu

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

  // real ops
}

W ten sposób prywatny konstruktor jest uważany za przetestowany, a narzut czasu wykonania jest w zasadzie nie do zmierzenia. Robię to, aby uzyskać 100% pokrycie przy użyciu EclEmma, ​​ale prawdopodobnie działa to dla każdego narzędzia pokrycia. Wadą tego rozwiązania jest oczywiście to, że piszesz kod produkcyjny (statyczny inicjalizator) tylko do celów testowych.


Często to robię. Tanie jak niedrogie, tanie jak brudne, ale skuteczne.
pholser

W przypadku Sonara powoduje to w rzeczywistości całkowite pominięcie klasy przez pokrycie kodu.
Adam Parkin

1

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


To powinna być prawidłowa odpowiedź, ponieważ odpowiada dokładnie na to, o co pytamy.
Chakian,

0

Czasami Cobertura oznacza kod, który nie jest przeznaczony do wykonania, jako „nieobjęty”, nie ma w tym nic złego. Dlaczego martwisz się posiadaniem 99%ubezpieczenia zamiast 100%?

Technicznie rzecz biorąc, nadal możesz wywołać tego konstruktora z refleksją, ale dla mnie brzmi to bardzo źle (w tym przypadku).


0

Gdybym miał odgadnąć cel twojego pytania, powiedziałbym:

  1. Chcesz rozsądnych kontroli dla prywatnych konstruktorów, którzy wykonują rzeczywistą pracę, i
  2. Chcesz, aby koniczyna wykluczyła puste konstruktory dla klas util.

W przypadku 1 jest oczywiste, że chcesz, aby cała inicjalizacja była wykonywana metodami fabrycznymi. W takich przypadkach twoje testy powinny być w stanie przetestować skutki uboczne konstruktora. Powinno to należeć do kategorii zwykłych prywatnych metod testowania. Zmniejsz metody, tak aby wykonywały tylko ograniczoną liczbę określonych rzeczy (najlepiej tylko jedną rzecz i dobrze jedną rzecz), a następnie przetestuj metody, które na nich polegają.

Na przykład, jeśli mój [prywatny] konstruktor ustawia pola instancji mojej klasy ana 5. Wtedy mogę (a raczej muszę) to przetestować:

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

W przypadku wersji 2 możesz skonfigurować clover, aby wykluczał konstruktory Util, jeśli masz ustawiony wzorzec nazewnictwa dla klas Util. Np. W moim własnym projekcie używam czegoś takiego (ponieważ przestrzegamy konwencji, że nazwy wszystkich klas Util powinny kończyć się na Util):

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

Celowo pominąłem .*następujące, )ponieważ takie konstruktory nie są przeznaczone do rzucania wyjątków (nie mają nic robić).

Oczywiście może istnieć trzeci przypadek, w którym możesz chcieć mieć pusty konstruktor dla klasy nieużytkowej. W takich przypadkach radziłbym umieścić methodContextdokładny podpis konstruktora.

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

Jeśli masz wiele takich wyjątkowych klas, możesz zmodyfikować uogólniony prywatny konstruktor reg-ex, który zasugerowałem, i usunąć Utilgo. W takim przypadku będziesz musiał ręcznie upewnić się, że efekty uboczne twojego konstruktora są nadal testowane i objęte innymi metodami w twojej klasie / projekcie.

<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 to plik źródłowy, który zawiera Twój prywatny konstruktor


Byłoby miło wyjaśnić, dlaczego ta konstrukcja pomaga w pokryciu.
Markus

To prawda, a po drugie: po co łapać wyjątek w swoim teście? Zgłoszony wyjątek powinien faktycznie spowodować niepowodzenie testu.
Jordi

0

Poniższe działały dla mnie na klasie utworzonej z adnotacją Lombok @UtilityClass, która automatycznie dodaje prywatnego konstruktora.

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

Chociaż constructor.setAccessible (true) powinno działać, gdy prywatny konstruktor został napisany ręcznie, z adnotacją Lombok nie działa, ponieważ wymusza to. Constructor.newInstance () faktycznie sprawdza, czy konstruktor jest wywoływany i to kończy pokrycie samego costructor. Dzięki assertThrows zapobiegasz niepowodzeniu testu i zarządzasz wyjątkiem, ponieważ jest to dokładnie oczekiwany błąd. Chociaż jest to obejście i nie doceniam koncepcji „pokrycia linii” w porównaniu z „pokryciem funkcjonalności / zachowania”, możemy znaleźć sens w tym teście. W rzeczywistości jesteś pewien, że Klasa Użytkowa ma w rzeczywistości prywatnego Konstruktora, który poprawnie zgłasza wyjątek, gdy jest wywoływany również przez reflaction. Mam nadzieję że to pomoże.


Cześć @ShanteshwarInde. Wielkie dzięki. Mój wkład został zmieniony i uzupełniony zgodnie z twoimi sugestiami. Pozdrowienia.
Riccardo Solimena

0

Moja preferowana opcja w 2019 roku: użyj lombok.

W szczególności @UtilityClassadnotacja . (Niestety w chwili pisania tego tekstu było to tylko „eksperymentalne”, ale działa dobrze i ma pozytywne perspektywy, więc prawdopodobnie wkrótce zostanie zaktualizowane do wersji stabilnej).

Ta adnotacja doda prywatny konstruktor, aby zapobiec tworzeniu instancji i sprawi, że klasa będzie ostateczna. W połączeniu z lombok.addLombokGeneratedAnnotation = truein lombok.config, prawie wszystkie platformy testowe zignorują automatycznie wygenerowany kod podczas obliczania pokrycia testu, umożliwiając ominięcie pokrycia tego automatycznie wygenerowanego kodu bez hacków lub odbić.


-2

Nie możesz.

Najwyraźniej tworzysz konstruktora prywatnego, aby zapobiec tworzeniu instancji klasy, która ma zawierać tylko metody statyczne. Zamiast próbować uzyskać pokrycie tego konstruktora (co wymagałoby utworzenia instancji klasy), powinieneś się go pozbyć i ufać programistom, że nie dodają metod instancji do klasy.


3
To jest niepoprawne; można go utworzyć za pomocą odbicia, jak wspomniano powyżej.
theotherian

To źle, nigdy nie pozwól, aby pojawił się domyślny konstruktor publiczny, powinieneś dodać prywatny, aby zapobiec jego wywołaniu.
Lho Ben
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.