개인 생성자에 테스트 커버리지를 추가하는 방법은 무엇입니까?


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

잘 작동하고 클래스가 테스트됩니다. 그러나 Cobertura는 클래스의 개인 생성자에 대한 코드 커버리지가 없다고 말합니다. 이러한 개인 생성자에 테스트 커버리지를 어떻게 추가 할 수 있습니까?


Singleton 패턴을 시행하려는 것처럼 보입니다. 그렇다면 dp4j.com을 좋아할 것입니다 (정확히 수행)
simpatico

"의도적으로 비어 있음"을 예외 발생으로 바꾸면 안 되나요? 이 경우 특정 메시지와 함께 특정 예외를 예상하는 테스트를 작성할 수 있습니다. 이 과잉 확실하지 경우
Ewoks

답변:


85

글쎄, 당신이 잠재적으로 리플렉션 등을 사용할 수있는 방법이 있습니다-하지만 그만한 가치가 있습니까? 이것은 절대 호출 되어서는 안되는 생성자입니다 .

Cobertura가 호출되지 않는다는 것을 이해하도록 클래스에 추가 할 수있는 주석 또는 이와 유사한 것이있는 경우 그렇게하십시오. 인위적으로 커버리지를 추가하기 위해 후프를 거칠 가치가 없다고 생각합니다.

편집 : 그것을 할 방법이 없다면 약간 감소 된 범위로 생활하십시오. 커버리지는 당신에게 유용한 것임을 기억하세요. 당신은 도구를 담당해야합니다.


18
이 특정 생성자 때문에 전체 프로젝트에서 "보도 범위를 약간 감소"하고 싶지 않습니다.
yegor256 dec.

36
@Vincenzo : 그렇다면 IMO는 단순한 숫자에 너무 높은 가치를 부여하고 있습니다. 커버리지는 테스트 의 지표 입니다. 도구의 노예가되지 마십시오. 커버리지의 요점은 신뢰 수준을 제공하고 추가 테스트를위한 영역을 제안하는 것입니다. 사용하지 않는 생성자를 인위적으로 호출하는 것은 이러한 점 중 하나에 도움이되지 않습니다.
Jon Skeet

19
@JonSkeet : "도구의 노예가되지 말라"는 데 전적으로 동의하지만 모든 프로젝트에서 모든 "결함 수"를 기억하는 것은 좋은 냄새가 아닙니다. 7/9 결과가 프로그래머가 아닌 Cobertura 제한인지 확인하는 방법은 무엇입니까? 새로운 프로그래머는 클래스별로 확인하기 위해 모든 실패 (대규모 프로젝트에서는 많을 수 있음)를 입력해야합니다.
Eduardo Costa

5
이것은 질문에 대한 답이 아닙니다. 그런데 일부 관리자는 커버리지 번호를 봅니다. 이유는 상관하지 않습니다. 그들은 85 %가 75 %보다 낫다는 것을 알고 있습니다.
ACV

2
액세스 할 수없는 코드를 테스트하는 실제 사용 사례는 아무도 해당 클래스를 다시 볼 필요가 없도록 100 % 테스트 커버리지를 달성하는 것입니다. 커버리지가 95 %에 머물러있는 경우 많은 개발자가이 문제에 계속해서 부딪히기 위해 그 이유를 파악하려고 시도 할 수 있습니다.
thisismydesign 2005

140

나는 Jon Skeet에 전적으로 동의하지 않습니다. 커버리지를 제공하고 커버리지 보고서의 소음을 제거 할 수있는 쉬운 승리를 얻을 수 있다면 그렇게해야한다고 생각합니다. 커버리지 도구에 생성자를 무시하도록 지시하거나 이상주의를 제쳐두고 다음 테스트를 작성하여 수행하십시오.

@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
그러나 이것은 테스트 스위트에 노이즈추가 하여 커버리지 보고서의 노이즈를 제거합니다 . 나는 "이상주의를 제쳐두십시오"에서 문장을 막 끝냈을 것입니다. :)
Christopher Orr

11
이 테스트에 어떤 의미를 부여하려면 생성자의 액세스 수준이 예상 한 수준이라고 주장해야합니다.
Jeremy

"testIfConstructorIsPrivateWithoutRaisingExceptions"와 같은 의미있는 이름과 함께 제레미의 생각과 사악한 반사를 더하면 이것이 "THE"대답이라고 생각합니다.
Eduardo Costa

1
이것은 구문 적으로 틀린 것입니까? 무엇입니까 constructor? Constructor매개 변수화되지 않아야하고 원시 유형 이 아니어야합니까 ?
Adam Parkin 2013 년

2
이것은 잘못된 것입니다. constructor.isAccessible()공개 생성자에서도 항상 false를 반환합니다. 하나는 assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen 2013 년

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과 같이 사용하도록 접근성 테스트를 변경해야했습니다 (라이브러리 간섭 조롱?). isAccessibletrue
David Harkness 2011

4
나는 이것을 JUnit의 Assert 클래스에 추가하고 싶지만 작업에 대한 크레딧을 받고 싶지는 않습니다. 아주 좋은 것 같아요. 가지고 좋은 것 Assert.utilityClassWellDefined()JUnit을 4.12+에. 풀 요청을 고려해 보셨습니까?
Visionary Software Solutions

setAccessible()생성자를 액세스 가능하게 만드는 데 사용 하면 Sonar의 코드 검사 도구에 문제가 발생합니다 (이 작업을 수행하면 Sonar의 코드 검사 보고서에서 클래스가 사라집니다).
Adam Parkin 2013 년

감사합니다. 그래도 액세스 가능한 플래그를 재설정합니다. 아마도 Sonar 자체의 버그일까요?
Archimedes Trajano 2013 년

내 바틱 메이븐 플러그인에 대한 커버리지에 대한 내 Sonar 보고서를 살펴 보았는데 올바르게 커버하는 것 같습니다. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano 2014-04-04

19

CheckStyle을 만족시키기 위해 내 정적 유틸리티 함수 클래스의 생성자를 private으로 만들었습니다. 그러나 원래 포스터와 마찬가지로 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 % 메서드 커버리지의 따뜻한 퍼지 느낌이 좋아요.


그것은있을 수 있습니다 잔인하지만 Unitils 또는 유사한 있다면, 나는 그것을 사용하십시오
스튜어트

+1 좋은 시작, 비록 내가 아르키메데스의 더 완전한 테스트로 갔지만 .
David Harkness

첫 번째 예제는 작동하지 않습니다. IllegalAccesException은 생성자가 호출되지 않으므로 적용 범위가 기록되지 않음을 의미합니다.
Tom McIntyre 2013 년

IMO, 첫 번째 코드 스 니펫의 솔루션은이 토론에서 가장 깨끗하고 간단한 솔루션입니다. 그냥 줄 fail(...)이 필요하지 않습니다.
Piotr Wittchen

9

Java 8을 사용하면 다른 솔루션을 찾을 수 있습니다.

공개 정적 메서드가 거의없는 유틸리티 클래스를 간단히 만들고 싶다고 가정합니다. Java 8을 사용할 수 있으면 interface대신 사용할 수 있습니다 .

package com.XXX;

public interface Foo {

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

Cobertura에서는 생성자도없고 불평도 없습니다. 이제 정말 관심있는 라인 만 테스트하면됩니다.


1
그러나 아쉽게도 인터페이스를 "최종"으로 선언 할 수 없어서 누구도 서브 클래 싱하지 못하도록 할 수 없습니다. 그렇지 않으면 이것이 최선의 방법입니다.
Michael Berry

5

아무것도하지 않는 코드를 테스트하는 이유는 100 % 코드 커버리지를 달성하고 코드 커버리지가 떨어질 때를 알아 차리는 것입니다. 그렇지 않으면 항상 생각할 수 있습니다. 더 이상 100 % 코드 커버리지가 없지만 개인 생성자 때문에 가능성이 있습니다. 이렇게하면 개인 생성자인지 확인하지 않고도 테스트되지 않은 메서드를 쉽게 찾을 수 있습니다. 코드베이스가 커짐에 따라 실제로 99 %가 아닌 100 %를 보면 따뜻한 느낌이들 것입니다.

IMO에서는 리플렉션을 사용하는 것이 가장 좋습니다. 특정 코드 커버리지 도구로.

완벽한 세상에서 모든 코드 커버리지 도구는 최종 클래스에 속하는 개인 생성자를 무시할 것입니다. 생성자가 "보안"측정 값으로 다른 것은 없기 때문입니다.)
이 코드를 사용합니다.

    @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

사소한 것 무시

Ignore trivial은 코드 한 줄을 포함하는 생성자 / 메소드를 제외하는 기능을 허용합니다. 몇 가지 예에는 super constrctor에 대한 호출, getter / setter 메소드 등이 포함됩니다. ignore 간단한 인수를 포함하려면 다음을 추가하십시오.

<cobertura-instrument ignoreTrivial="true" />

또는 Gradle 빌드에서 :

cobertura {
    coverageIgnoreTrivial = true
}

4

하지마. 빈 생성자를 테스트하는 이유는 무엇입니까? cobertura 2.0부터 이러한 사소한 경우를 무시할 수있는 옵션이 있습니다 (setter / getter와 함께), cobertura maven 플러그인에 구성 섹션을 추가하여 maven에서 활성화 할 수 있습니다.

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

또는 당신이 사용할 수있는 범위 주석 : @CoverageIgnore.


3

마지막으로 해결책이 있습니다!

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

그래도 질문에 게시 된 클래스를 어떻게 테스트합니까? 개인 생성자가있는 모든 클래스를 열거 형으로 바꿀 수 있다고 가정해서는 안됩니다.
Jon Skeet

@JonSkeet 문제의 수업을 위해 할 수 있습니다. 그리고 많은 정적 메서드 만있는 대부분의 유틸리티 클래스. 그렇지 않으면 유일한 개인 생성자를 가진 클래스는 의미가 없습니다.
kan

1
개인 생성자 있는 클래스는 공용 정적 메서드에서 인스턴스화 수 있지만 물론 적용 범위를 쉽게 얻을 수 있습니다. 그러나 근본적으로 나는 Enum<E>실제로 열거 형으로 확장 되는 클래스를 선호합니다 ... 의도를 더 잘 드러내는 것으로 믿습니다.
Jon Skeet

4
와우, 저는 꽤 임의의 숫자보다 의미있는 코드를 절대적으로 선호합니다. (보증은 품질을 보장하지 않으며 모든 경우에 100 % 적용 가능합니다. 테스트는 기껏해야 코드를 가이드 해야합니다. 기괴한 의도의 절벽을 넘어서는 안됩니다.)
Jon Skeet

1
@Kan : 생성자에 더미 호출을 추가하여 도구를 블러핑하는 것이 의도가되어서는 안됩니다. 프로젝트의 웰빙을 결정하기 위해 단일 지표에 의존하는 사람은 이미 파괴의 길에 있습니다.
Apoorv Khurasia 2012 년

1

Cobertura에 대해 잘 모르지만 Clover를 사용하며 패턴 일치 제외를 추가하는 수단이 있습니다. 예를 들어, apache-commons-logging 라인을 제외하는 패턴이 있으므로 적용 범위에 포함되지 않습니다.


1

또 다른 옵션은 다음 코드와 유사한 정적 이니셜 라이저를 만드는 것입니다.

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

  // real ops
}

이렇게하면 개인 생성자가 테스트 된 것으로 간주되고 런타임 오버 헤드는 기본적으로 측정 할 수 없습니다. EclEmma를 사용하여 100 % 커버리지를 얻기 위해이 작업을 수행하지만 모든 커버리지 도구에서 효과가있을 수 있습니다. 물론이 솔루션의 단점은 테스트 목적으로 만 프로덕션 코드 (정적 이니셜 라이저)를 작성한다는 것입니다.


나는 이것을 꽤 많이한다. 저렴하고 더럽고 저렴하지만 효과적입니다.
pholser 2011-06-17

Sonar를 사용하면 실제로 코드 커버리지에 의해 클래스가 완전히 누락됩니다.
Adam Parkin

1

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


요청 된 내용에 정확히 대답하므로 정답이어야합니다.
Chakian

0

때때로 Cobertura는 실행되도록 의도되지 않은 코드를 '덮어지지 않음'으로 표시합니다. 99%대신 보험 적용에 관심이있는 이유100% 입니까?

기술적으로는 여전히 리플렉션을 사용하여 해당 생성자를 호출 할 수 있지만이 경우에는 매우 잘못된 것 같습니다.


0

질문의 의도를 추측한다면 다음과 같이 말하고 싶습니다.

  1. 실제 작업을 수행하는 개인 생성자에 대한 합리적인 검사를 원합니다.
  2. clover가 util 클래스에 대해 빈 생성자를 제외하려고합니다.

1의 경우 모든 초기화가 공장 방법을 통해 수행되기를 원한다는 것이 분명합니다. 이러한 경우 테스트는 생성자의 부작용을 테스트 할 수 있어야합니다. 이것은 일반적인 비공개 메서드 테스트의 범주에 속해야합니다. 제한된 수의 결정적인 작업 만 수행하도록 메서드를 더 작게 만든 다음 (이상적으로는 한 가지, 한 가지만 잘 수행) 이에 의존하는 메서드를 테스트합니다.

예를 들어, 내 [private] 생성자가 내 클래스의 인스턴스 필드 a5. 그런 다음 테스트 할 수 있습니다 (또는 테스트해야합니다).

@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 생성자를 제외하도록 clover를 구성 할 수 있습니다. 예를 들어, 내 프로젝트에서 다음과 같은 것을 사용합니다 (모든 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는 개인 생성자가있는 소스 파일입니다.


왜이 구조가 적용 범위에 도움이되는지 설명하는 것이 좋을 것입니다.
Markus

사실, 두 번째 : 테스트에서 예외를 포착하는 이유는 무엇입니까? throw되는 예외로 인해 실제로 테스트가 실패해야합니다.
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 ()는 실제로 생성자가 호출되었는지 테스트하고 이것은 costructor 자체의 범위를 완료합니다. assertThrows를 사용하면 테스트가 실패하는 것을 방지하고 예상 한 오류와 정확히 일치하므로 예외를 관리했습니다. 이것은 해결 방법이고 "라인 범위"대 "기능 / 행동 범위"의 개념을 이해하지 못하지만이 테스트에서 의미를 찾을 수 있습니다. 실제로 유틸리티 클래스에는 reflaction을 통해 호출 될 때 예외를 올바르게 throw하는 개인 생성자가 실제로 있다고 확신합니다. 도움이 되었기를 바랍니다.


안녕하세요 @ShanteshwarInde. 감사합니다. 내 의견은 귀하의 제안에 따라 수정 및 완료되었습니다. 문안 인사.
Riccardo Solimena

0

2019 년에 제가 선호하는 옵션 : 롬복을 사용하세요.

특히 @UtilityClass주석 . (슬프게도 글을 쓰는 시점에는 "실험적"일 뿐이지 만 잘 작동하고 긍정적 인 전망을 가지고 있으므로 곧 안정으로 업그레이드 될 가능성이 높습니다.)

이 주석은 인스턴스화를 방지하기 위해 개인 생성자를 추가하고 클래스를 최종적으로 만듭니다. lombok.addLombokGeneratedAnnotation = truein 과 결합하면 lombok.config거의 모든 테스트 프레임 워크가 테스트 범위를 계산할 때 자동 생성 된 코드를 무시하므로 해킹이나 반사없이 자동 생성 된 코드의 적용 범위를 우회 할 수 있습니다.


-2

당신은 할 수 없습니다.

정적 메서드 만 포함하도록 의도 된 클래스의 인스턴스화를 방지하기 위해 개인 생성자를 만드는 것 같습니다. 이 생성자 (클래스가 인스턴스화되어야 함)에 대한 커버리지를 얻으려고 시도하는 대신, 생성자를 제거하고 개발자가 클래스에 인스턴스 메서드를 추가하지 않도록 신뢰해야합니다.


3
그것은 틀 렸습니다. 위에서 언급했듯이 리플렉션을 통해 인스턴스화 할 수 있습니다.
theotherian

그것은 나쁘다. 기본 공개 생성자가 나타나지 않도록하라. 호출을 막기 위해 비공개 생성자를 추가해야한다.
Lho Ben
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.