Как добавить тестовое покрытие в частный конструктор?

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

Работает нормально, класс протестирован. Но Кобертура говорит, что закрытый конструктор класса не покрывается кодом. Как мы можем добавить тестовое покрытие к такому частному конструктору?

yegor256
источник
Мне кажется, что вы пытаетесь применить шаблон Singleton. Если да, то вам может понравиться dp4j.com (который делает именно это)
simpatico
не следует ли «намеренно пустой» заменять генерирующим исключением? В этом случае вы могли бы написать тест, ожидающий этого конкретного исключения с конкретным сообщением, не так ли? не уверен, что это перебор
Эвоки

Ответы:

85

Что ж, есть способы потенциально использовать отражение и т. Д. - но действительно ли оно того стоит? Это конструктор, который нельзя вызывать , верно?

Если есть аннотация или что-то подобное, что вы можете добавить к классу, чтобы Cobertura понимала, что он не будет вызываться, сделайте это: я не думаю, что стоит пытаться добавить покрытие искусственно.

РЕДАКТИРОВАТЬ: Если нет возможности сделать это, просто живите с немного уменьшенным покрытием. Помните, что покрытие должно быть для вас полезным - вы должны отвечать за инструмент, а не наоборот.

Джон Скит
источник
18
Я не хочу «немного уменьшать покрытие» всего проекта только из-за этого конкретного конструктора ..
yegor256
36
@Vincenzo: Тогда, ИМО, вы слишком высоко цените простое число. Покрытие - это показатель тестирования. Не будьте рабом инструмента. Точка охвата - дать вам уверенность и предложить области для дополнительного тестирования. Искусственный вызов неиспользуемого конструктора не помогает ни в одном из этих вопросов.
Джон Скит,
19
@JonSkeet: Я полностью согласен с «Не будьте рабом инструмента», но неприятно запоминать каждый «счетчик недостатков» в каждом проекте. Как убедиться, что результат 7/9 является ограничением Cobertura, а не программистом? Новый программист должен вводить каждую ошибку (а в больших проектах может быть много), чтобы проверять класс за классом.
Эдуардо Коста
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", я думаю, это "САМЫЙ" ответ.
Эдуардо Коста
1
Это синтаксически неверно, не так ли? Что есть constructor? Не Constructorследует параметризовать и не использовать исходный тип?
Адам Паркин
2
Это неправильно: constructor.isAccessible()всегда возвращает false, даже в общедоступном конструкторе. Надо использовать 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).
Адам Паркин
Спасибо, я сбросил флаг доступности. Может, это ошибка самого Сонара?
Archimedes Trajano
Я просмотрел свой отчет о сонаре, чтобы узнать о моем плагине 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");
}

Я пошел с предложением Джавида Джамаэ и использовал отражение, но добавил утверждения, чтобы поймать кого-нибудь, кто возится с тестируемым классом (и назвал тест, чтобы указать высокие уровни зла).

@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 или подобном, я бы использовал его
Стюарт
+1 Хорошее начало, хотя я пошел с более полным тестом Архимеда .
Дэвид Харкнесс
Первый пример не работает - IllegalAccesException означает, что конструктор никогда не вызывается, поэтому покрытие не записывается.
Том Макинтайр
ИМО, решение в первом фрагменте кода является самым чистым и простым в этом обсуждении. Просто соответствовать fail(...)не обязательно.
Петр
9

С Java 8 можно найти другое решение.

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

package com.XXX;

public interface Foo {

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

Конструктора и претензий от Cobertura нет. Теперь вам нужно протестировать только те строки, которые вам действительно нужны.

Арност Валичек
источник
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());
        }
    }
А затем просто добавляйте классы в массив по ходу.

jontejj
источник
5

Более новые версии Cobertura имеют встроенную поддержку игнорирования тривиальных геттеров / сеттеров / конструкторов:

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

Игнорировать тривиальное

Игнорировать тривиальный позволяет исключить конструкторы / методы, содержащие одну строку кода. Некоторые примеры включают вызов только суперконструктора, методов получения / установки и т. Д. Чтобы включить тривиальный аргумент ignore, добавьте следующее:

<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 Я могу для рассматриваемого класса. И большинство служебных классов, которые имеют только набор статических методов. В противном случае класс с единственным частным конструктором не имеет смысла.
kan
1
Класс с частным конструктором может быть создан из общедоступных статических методов, хотя, конечно, легко получить покрытие. Но принципиально я бы предпочел, чтобы любой класс, который расширяется, Enum<E>действительно был перечислением ... Я считаю, что это лучше раскрывает намерение.
Джон Скит
4
Ничего себе, я бы абсолютно предпочитаю код , который имеет смысл в течение довольно произвольное число. (Покрытие не является гарантией качества, и не во всех случаях возможно 100% покрытие. Ваши тесты должны в лучшем случае направлять ваш код, а не направлять его через обрыв странных намерений.)
Джон Скит
1
@Kan: Добавление фиктивного вызова к конструктору, чтобы обмануть инструмент, не должно быть намерением. Любой, кто полагается на одну метрику для определения благополучия проекта, уже находится на пути к разрушению.
Апурв Хорасия,
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);

acpuma
источник
Это должен был быть правильный ответ, поскольку он отвечает в точности на заданный вопрос.
Chakian 07
0

Иногда Cobertura отмечает код, не предназначенный для выполнения, как «не покрытый», в этом нет ничего плохого. Почему вас беспокоит 99%страховое покрытие вместо 100%?

Однако технически вы все еще можете вызывать этот конструктор с отражением, но для меня это звучит очень неправильно (в данном случае).

Никита Рыбак
источник
0

Если бы я угадал цель вашего вопроса, я бы сказал:

  1. Вам нужны разумные проверки для частных конструкторов, которые действительно работают, и
  2. Вы хотите, чтобы клевер исключал пустые конструкторы для служебных классов.

Для 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 вы можете настроить clover для исключения конструкторов 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 - это ваш исходный файл, в котором есть ваш частный конструктор

DPREDDY
источник
Было бы неплохо объяснить, почему эта конструкция помогает с покрытием.
Маркус
Верно, а во-вторых: зачем ловить исключение в вашем тесте? Выброшенное исключение должно фактически привести к сбою теста.
Jordi
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 вы предотвращаете сбой теста и управляете исключением, поскольку это именно та ошибка, которую вы ожидаете. Хотя это обходной путь, и я не ценю концепцию «покрытия строки» и «покрытия функциональности / поведения», мы можем найти смысл в этом тесте. Фактически, вы уверены, что у служебного класса есть частный конструктор, который правильно генерирует исключение при вызове также через рефлексию. Надеюсь это поможет.

Риккардо Солимена
источник
Привет, @ShanteshwarInde. Большое спасибо. Мой вклад был отредактирован и дополнен вашими предложениями. С уважением.
Риккардо Солимена,
0

Мой предпочтительный вариант в 2019 году: использовать ломбок.

В частности, @UtilityClassаннотация . (К сожалению, только «экспериментальный» на момент написания, но он работает нормально и имеет положительные перспективы, поэтому, вероятно, скоро будет обновлен до стабильной версии.)

Эта аннотация добавит частный конструктор для предотвращения создания экземпляра и сделает класс окончательным. В сочетании с lombok.addLombokGeneratedAnnotation = truein lombok.configпрактически все среды тестирования будут игнорировать автоматически сгенерированный код при вычислении покрытия тестами, что позволяет обойти покрытие этого автоматически сгенерированного кода без взлома или отражения.

Майкл Берри
источник
-2

Вы не можете.

Очевидно, вы создаете частный конструктор, чтобы предотвратить создание экземпляра класса, который должен содержать только статические методы. Вместо того, чтобы пытаться охватить этот конструктор (что потребовало бы создания экземпляра класса), вы должны избавиться от него и доверять своим разработчикам, чтобы они не добавляли методы экземпляра в класс.

Анон
источник
3
Это неверно; вы можете создать его с помощью отражения, как указано выше.
theotherian
Это плохо, никогда не позволяйте отображать общедоступный конструктор по умолчанию, вы должны добавить частный, чтобы предотвратить его вызов.
Лхо Бен