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

Это код:

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

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

Ответ 1

Ну, есть способы, которыми вы могли бы использовать отражение и т.д. - но действительно ли это стоит? Это конструктор, который никогда не следует вызывать, правильно?

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

EDIT: Если нет способа сделать это, просто живете со слегка уменьшенным охватом. Помните, что охват призван стать чем-то полезным для вас - вы должны отвечать за инструмент, а не наоборот.

Ответ 2

Я не совсем согласен с Джоном Скитом. Я думаю, что если вы можете получить легкую победу, чтобы дать вам освещение и устранить шум в своем отчете о покрытии, тогда вы должны это сделать. Либо сообщите инструменту покрытия игнорировать конструктор, либо отложите идеализм и напишите следующий тест и сделайте с ним:

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

Ответ 3

Несмотря на то, что это не обязательно для покрытия, я создал этот метод, чтобы убедиться, что класс утилиты хорошо определен и немного освещен.

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

Ответ 4

Я сделал private конструктором моего класса статических функций полезности, чтобы удовлетворить CheckStyle. Но, как и оригинальный плакат, у меня был Cobertura, жалующийся на тест. Сначала я пробовал этот подход, но это не влияет на отчет о покрытии, потому что конструктор никогда не выполняется. Так что действительно все эти тесты, если конструктор остается закрытым - и это делается избыточным благодаря проверке доступности в последующем тесте.

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

Я пошел с предложением Javid Jamae и использовал рефлексию, но добавил утверждения, чтобы поймать кого-нибудь, кто возится с тестируемым классом (и назвал тест, чтобы указать 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());
}

Это так излишне, но я должен признать, что мне нравится теплое нечеткое ощущение 100% -ного покрытия метода.

Ответ 5

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

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

package com.XXX;

public interface Foo {

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

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

Ответ 6

Обоснование кода тестирования, которое ничего не делает, - это обеспечить 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());
        }
    }
А потом просто добавьте классы в массив, когда идете.

Ответ 7

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

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

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

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

<cobertura-instrument ignoreTrivial="true" />

или в сборке Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

Ответ 8

не делать. Какой смысл при тестировании пустого конструктора? Поскольку cobertura 2.0 имеет возможность игнорировать такие тривиальные случаи (вместе с сеттерами/геттерами), вы можете включить его в maven, добавив раздел конфигурации в плагин cobertura maven:

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

В качестве альтернативы вы можете использовать аннотации покрытия: @CoverageIgnore.

Ответ 9

Наконец, есть решение!

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

Ответ 10

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

Ответ 11

Другой вариант - создать статический инициализатор, похожий на следующий код

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

  // real ops
}

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

Ответ 12

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

Ответ 13

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

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

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

Ответ 14

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

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

Ответ 15

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

  • Вам нужны разумные проверки для частных конструкторов, которые выполняют фактическую работу, и
  • Вы хотите, чтобы клевер исключил пустые конструкторы для классов util.

Для 1 очевидно, что вы хотите, чтобы вся инициализация выполнялась с помощью методов factory. В таких случаях ваши тесты должны быть в состоянии проверить побочные эффекты конструктора. Это должно подпадать под категорию обычного частного тестирования методов. Сделайте методы меньшими, чтобы они выполняли только ограниченное количество детерминированных вещей (в идеале, только одну вещь и одну вещь хорошо), а затем проверяли методы, которые полагаются на них.

Например, если мой конструктор [private] устанавливает мои экземпляры экземпляра класса 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>

Ответ 16

@Test
public void testTestPrivateConstructor() {
    Constructor<Test> cnt;
    try {
        cnt = Test.class.getDeclaredConstructor();
        cnt.setAccessible(true);

        cnt.newInstance();
    } catch (Exception e) {
        e.getMessage();
    }
}

Test.java - это ваш исходный файл, у которого есть свой частный конструктор

Ответ 17

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

@Inject
 private EventUtil(String abc)
    {
        this.abc=abc;
    }

Ответ 18

Вы не можете.

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

Ответ 19

Следующее работает для меня над классом, созданным с помощью аннотации 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 Class есть собственный конструктор, который корректно генерирует исключение при вызове также с помощью рефлексии. Надеюсь это поможет.