Як додати тестове покриття приватному конструктору?


110

Це код:

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

Це тест:

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

Працює чудово, клас перевірений. Але Кобертура говорить, що приватне конструктор класу не має кодового покриття. Як ми можемо додати тестове покриття до такого приватного конструктора?


Мені здається, ніби ви намагаєтесь застосувати схему Singleton. Якщо так, то, можливо, вам сподобається dp4j.com (що робить саме це)
simpatico

чи не слід «навмисно пустувати» замінювати виключенням кидання? У такому випадку ви можете написати тест, який очікує конкретного винятку з конкретним повідомленням, ні? не впевнений, чи це надмірність
Ewoks

Відповіді:


85

Що ж, є способи, якими ви потенційно можете використовувати рефлексію тощо - але чи варто цього дійсно? Це конструктор, якого ніколи не слід називати , правда?

Якщо у класі є примітка або щось подібне, що ви можете додати до класу, щоб Кобертура зрозуміла, що його не називатимуться, зробіть це: я не думаю, що варто обробляти обручі, щоб штучно додавати покриття.

РЕДАКТИРУВАТИ: Якщо немає можливості це зробити, просто живіть з дещо зменшеним покриттям. Пам’ятайте, що висвітлення має бути корисним для вас - вам слід керувати інструментом, а не навпаки.


18
Я не хочу "трохи зменшувати покриття" у всьому проекті саме завдяки цьому конкретному конструктору ..
yegor256

36
@ Вінченцо: Тоді IMO ви ставите занадто високе значення простому числу. Покриття є показником тестування. Не будьте рабом інструменту. Місце висвітлення полягає в тому, щоб надати вам рівень впевненості та запропонувати місця для додаткового тестування. Штучне виклик конструктора, який не використовується, не допоможе ні в одній із цих точок.
Джон Скіт

19
@JonSkeet: Я повністю погоджуюся з "Не будь рабом інструменту", але це не пахне добре пам'ятати про "кількість недоліків" у кожному проекті. Як переконатися, що результат 7/9 - це обмеження Кобертура, а не програміст? Новий програміст повинен входити в кожну помилку (що може бути багато у великих проектах), щоб перевірити по класах.
Едуардо Коста

5
Це не дає відповіді на запитання. і, до речі, деякі менеджери дивляться на номери покриття. Їм байдуже, чому. Вони знають, що на 85% краще 75%.
ACV

2
Практичний варіант використання для перевірки інакше недоступного коду - це досягти 100% тестового покриття, щоб нікому не довелося дивитись на цей клас ще раз. Якщо покриття затримано на 95%, багато розробників можуть спробувати з'ясувати причину, щоб просто натрапляти на цю проблему знову і знову.
thisismydesign

140

Я не повністю згоден з Джоном Скітом. Я думаю, що якщо ви зможете отримати легкий виграш, який допоможе вам охопити та усунути шум у звіті про покриття, тоді вам слід це зробити. Або скажіть своєму інструменту покриття, щоб ігнорувати конструктор, або відкладіть ідеалізм убік і напишіть наступний тест, і виконайте це:

@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
Але це усуває шум у звіті про покриття, додаючи шум до тестового набору. Я б тільки закінчив речення на "відклав ідеалізм убік". :)
Крістофер Орр

11
Щоб надати цьому тесту будь-якого сенсу, ви, ймовірно, також повинні стверджувати, що рівень доступу конструктора - це такий, який ви очікуєте від нього.
Джеремі

Додаючи злий роздум плюс ідеї Джеремі плюс значущу назву на кшталт "testIfConstructorIsPrivateWithoutRaisingExceptions", я думаю, це відповідь "THE".
Едуардо Коста

1
Це синтаксично неправильно, чи не так? Що таке constructor? Чи не повинні Constructorбути параметризовані та не вихідний тип?
Адам Паркін

2
Це неправильно: constructor.isAccessible()завжди повертає помилку, навіть на громадському конструкторі. Слід використовувати assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen

78

Хоча це не обов'язково для покриття, я створив цей метод, щоб переконатися, що клас утиліти добре визначений, а також трохи покриття.

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

Я розмістив повний код та приклади на https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test


11
+1 Це не тільки вирішує проблему, не обробляючи інструмент, але й повністю перевіряє стандарти кодування встановлення класу корисності. Мені довелося змінити тест доступності на використання, Modifier.isPrivateоскільки isAccessibleповертався trueдля приватних конструкторів у деяких випадках (глузує з втручання бібліотеки?).
Девід Харкнесс

4
Я дуже хочу додати це до класу Assert JUnit, але не хочу брати кредит за вашу роботу. Я думаю, що це дуже добре. Було б чудово мати Assert.utilityClassWellDefined()в JUnit 4.12+. Ви розглядали запит на тягнення?
Visionary Software Solutions

Зауважте, що використання setAccessible()дозволу на конструктор викликає проблеми для інструмента покриття коду Sonar (коли я це роблю, клас зникає із звітів про покриття коду Sonar).
Адам Паркін

Дякую, я все-таки скидаю доступний прапор. Можливо, це помилка на самому Сонарі?
Архімед Траяно

Я переглянув мій звіт Sonar для висвітлення мого плагіну batik maven, здається, він охоплює правильно. site.trajano.net/batik-maven-plugin/cobertura/index.html
Архімед

19

Я став приватним конструктором мого класу статичних функцій корисності, щоб задовольнити CheckStyle. Але, як і оригінальний плакат, у мене Кобертура скаржився на тест. Спочатку я спробував такий підхід, але це не впливає на звіт про покриття, оскільки конструктор ніколи фактично не виконується. Тож справді все це тести, якщо конструктор залишається приватним - і це робиться надмірним шляхом перевірки доступності в наступному тесті.

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

Я пішов із пропозицією Джавіда Джама і використав роздуми, але додав твердження, щоб зловити кого-небудь возитися з тестуваним класом (і назвав тест для позначення High Levels 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());
}

Це так непосильно, але я повинен визнати, що мені подобається тепле нечітке відчуття 100% покриття методу.


Це може бути перебій, але якби він був у Unitils або подібному, я би використовував його
Stewart

+1 Добрий початок, хоча я пройшов більш повний тест Архімеда .
Девід Харкнесс

Перший приклад не працює - IllegalAccesException означає, що конструктор ніколи не викликається, тому покриття не реєструється.
Том Макінтайр

IMO, рішення першого фрагмента коду є найчистішим та найпростішим у цій дискусії. Просто рядок з fail(...)не потрібен.
Пьотр Віттхен

9

З Java 8 можна знайти інше рішення.

Я припускаю, що ви просто хочете створити клас корисності за допомогою кількох публічних статичних методів. Якщо ви можете використовувати Java 8, ви можете використовувати interfaceзамість цього.

package com.XXX;

public interface Foo {

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

У Кобертура немає конструктора і не скаржиться. Тепер вам потрібно перевірити лише ті лінії, які вам справді цікаві.


1
На жаль, ви не можете оголосити інтерфейс як "остаточний", заважаючи комусь підкласифікувати його - інакше це був би найкращий підхід.
Майкл Беррі

5

Обґрунтування тестування коду, який нічого не робить, - це досягти 100% покриття коду та помітити, коли покриття коду падає. Інакше завжди можна подумати, ей, я вже не маю 100% покриття коду, але це ПРОБАВНО через моїх приватних конструкторів. Це дозволяє легко помітити неперевірені методи, не перевіряючи, що це просто приватний конструктор. Коли ваша база даних зростає, ви насправді відчуєте приємне тепле відчуття, дивлячись на 100% замість 99%.

ІМО найкраще тут використовувати відображення, оскільки в іншому випадку вам доведеться або отримати кращий інструмент покриття коду, який ігнорує ці конструктори, або якось сказати інструменту покриття коду, щоб ігнорувати метод (можливо, Анотація або файл конфігурації), оскільки тоді ви застрягли б за допомогою конкретного інструменту покриття коду.

У ідеальному світі всі інструменти для покриття коду ігнорувалимуть приватні конструктори, які належать до остаточного класу, оскільки конструктор там є як міра "безпеки" нічого іншого :)
Я використовував би цей код:

    @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

Новіші версії Cobertura мають вбудовану підтримку ігнорування тривіальних геттерів / сеттерів / конструкторів:

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

Ігноруйте тривіальне

Ігнорувати тривіальне дозволяє можливість виключити конструктори / методи, які містять один рядок коду. Деякі приклади включають виклик лише до супер конструктора, методів getter / setter тощо. Для включення тривіального аргументу ігнорування додайте наступне:

<cobertura-instrument ignoreTrivial="true" />

або у складі Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

4

Не варто. Який сенс тестувати порожній конструктор? Оскільки cobertura 2.0 є можливість ігнорувати подібні тривіальні випадки (разом із сетерами / геттерами), ви можете ввімкнути це в maven, додавши розділ конфігурації до плагіну cobertura maven:

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

Як альтернативи ви можете використовувати Coverage анотації : @CoverageIgnore.


3

Нарешті, є рішення!

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

Як це тестування класу, який розміщено у запитанні? Ви не повинні вважати, що ви можете перетворити кожен клас із приватним конструктором на перелік, або що хочете.
Джон Скіт

@JonSkeet Я можу для відповідного класу. І більшість класів корисних програм, у яких є лише маса статичних методів. Інакше клас з єдиним приватним конструктором не має жодного сенсу.
кан

1
Клас з приватним конструктором може бути створений з публічних статичних методів, хоча, звичайно, тоді легко отримати охоплення. Але в принципі я вважаю за краще будь-який клас, який Enum<E>дійсно є перерахунком ... Я вважаю, що це виявляє наміри краще.
Джон Скіт

4
Ого, я б абсолютно віддав перевагу коду, який має сенс над досить довільним числом. (Покриття не є гарантією якості, а також не можливе 100% покриття у всіх випадках. Ваші тести повинні керувати кодом у кращому випадку - не керувати ним над скелею химерних намірів.)
Джон Скіт

1
@ Кан: Додавання фіктивного виклику до конструктора для блефування інструменту не повинно бути наміром. Кожен, хто покладається на одну метрику для визначення добробуту проекту, вже на шляху до руйнування.
Апоорв Хурасія

1

Я не знаю про Cobertura, але я використовую Clover, і він має засоби додавання виключень, що відповідають шаблонам. Наприклад, у мене є шаблони, які виключають рядки журналу apache-commons, тому вони не враховуються в покритті.


1

Інший варіант - створити статичний ініціалізатор, подібний до наступного коду

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

  // real ops
}

Таким чином, приватний конструктор вважається випробуваним, а накладні витрати в основному не піддаються вимірюванню. Я роблю це, щоб отримати 100% покриття за допомогою EclEmma, ​​але, швидше за все, це працює для кожного інструменту покриття. Недолік цього рішення, звичайно, полягає в тому, що ви пишете виробничий код (статичний ініціалізатор) саме для тестування.


Я роблю це зовсім небагато. Дешево, як у недорогих, дешевих, як у брудних, але ефективних.
pholser

З Sonar це насправді призводить до повного пропуску класу за допомогою покриття коду.
Адам Паркін

1

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


Це мала бути правильна відповідь, оскільки вона відповідає саме тому, що задається питанням.
Chakian

0

Іноді Кобертура позначає код, який не призначений для виконання як "не охоплений", в цьому немає нічого поганого. Чому ви переймаєтесь тим, щоб мати 99%покриття замість 100%?

Технічно, ви все ще можете викликати цей конструктор з відображенням, але це звучить мені дуже неправильно (в даному випадку).


0

Якби я вгадував наміри вашого запитання, я сказав би:

  1. Ви хочете обґрунтувати перевірку приватних будівельників, які виконують фактичну роботу, і
  2. Ви хочете, щоб конюшина виключала порожні конструктори для класів util.

Для 1 очевидно, що ви хочете, щоб уся ініціалізація проводилася фабричними методами. У таких випадках ваші тести повинні мати можливість перевірити побічні ефекти конструктора. Це має підпадати під категорію тестування звичайних приватних методів. Зробіть методи меншими, щоб вони виконували лише обмежену кількість визначальних речей (в ідеалі - лише одне і одне - добре), а потім протестуйте методи, які покладаються на них.

Наприклад, якщо мій [приватний] конструктор задає поля екземплярів мого класу aна 5. Тоді я можу (а точніше повинен) перевірити це:

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

Для 2 ви можете налаштувати конюшину для виключення конструкторів Util, якщо у вас є набір шаблонів імен для класів Util. Наприклад, у своєму власному проекті я використовую щось подібне (тому що ми дотримуємось конвенції, що імена для всіх класів Util повинні закінчуватися Util):

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

Я свідомо покинув .*наступне, )тому що такі конструктори не мають на меті викидати винятки (вони не мають на меті нічого робити).

Звичайно, може бути і третій випадок, коли ви можете мати порожній конструктор для не корисного класу. У таких випадках я рекомендую вам поставити methodContextточний підпис конструктора.

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

Якщо у вас багато таких виняткових класів, ви можете вибрати модифікований приватний конструктор reg-ex, який я запропонував, і видалити Utilз нього. У цьому випадку вам доведеться вручну переконатися, що побічні ефекти вашого конструктора все ще перевірені та охоплені іншими методами у вашому класі / проекті.

<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 - це ваш вихідний файл, який має ваш приватний конструктор


Було б непогано пояснити, чому ця конструкція допомагає при висвітленні.
Маркус

Щоправда, по-друге: навіщо ловити виняток у своєму тесті? Виняток, який викидається, насправді може зробити тест невдалим.
Джорді

0

Наступне працювало мені над класом, створеним за допомогою анотації Lombok @UtilityClass, яка автоматично додає приватний конструктор.

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

Хоча constructor.setAccessible (true) повинен працювати, коли приватний конструктор написаний вручну, анотація Lombok не працює, оскільки це примушує. Constructor.newInstance () фактично перевіряє виклик конструктора, і це завершує покриття самого конструктора. За допомогою assertThrows ви запобігаєте тому, що тест не вдався, і ви керували винятком, оскільки саме ви очікуєте помилку. Хоча це вирішення, і я не ціную концепцію "покриття лінії" проти "функціональність / охоплення поведінки", ми можемо знайти сенс у цьому тесті. Насправді ви впевнені, що клас Utility насправді має приватний конструктор, який правильно видає виняток, коли викликається також за допомогою рефлексії. Сподіваюся, це допомагає.


Привіт @ShanteshwarInde. Дуже дякую. Мій вклад було відредаговано та доповнено за вашими пропозиціями. З повагою
Ріккардо Солімена

0

Мій бажаний варіант у 2019 році: Використовуйте ломбок.

Зокрема, @UtilityClassанотація . (На жаль, лише "експериментальний" на момент написання, але він працює просто чудово і має позитивний прогноз, тому, швидше за все, незабаром буде оновлено до стабільного.)

Це анотація додасть приватний конструктор для запобігання інстанції та зробить клас остаточним. У поєднанні з lombok.addLombokGeneratedAnnotation = truein lombok.config, майже всі тестові рамки ігнорують автоматично згенерований код під час обчислення тестового покриття, що дозволяє вам обійти охоплення цього автоматично згенерованого коду без злому або відображення.


-2

Ви не можете.

Ви, мабуть, створюєте приватний конструктор, щоб запобігти екземпляру класу, який повинен містити лише статичні методи. Замість того, щоб намагатись висвітлити цей конструктор (що вимагатиме примірника класу), слід позбутися від нього і довіряти розробникам не додавати методи екземпляра до класу.


3
Це неправильно; Ви можете створити це через відображення, як зазначалося вище.
теоретичний

Це погано, ніколи не дозволяйте публічному конструктору за замовчуванням з’являтися, ви повинні додати приватний, щоб не викликати його.
Lho Ben
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.