如何为私有构造函数添加测试覆盖率?


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

Answers:


85

好吧,有很多方法可以潜在地使用反射等-真的值得吗?这是一个永远不应该被调用的构造函数,对吗?

如果您可以在类中添加注释或类似内容,以使Cobertura理解不会被调用,请执行以下操作:我认为不值得人工添加覆盖范围。

编辑:如果没有办法,只需稍微减少覆盖范围即可。请记住,覆盖范围是对有用的东西-您应该负责该工具,而不是相反。


18
我不想在仅仅因为这个特殊构造的整个项目“稍微减少覆盖” ..
yegor256

36
@Vincenzo:然后IMO,您在一个简单的数字上设置了太高的值。覆盖率是测试的指标。不要成为工具的奴隶。覆盖的重点是给您一定的信心,并建议需要额外测试的地方。人为地调用否则未使用的构造函数对这些要点都无济于事。
乔恩·斯基特

19
@JonSkeet:我完全同意“不要成为工具的奴隶”,但是记住每个项目中的每个“缺陷计数”都不好闻。如何确保7/9结果是Cobertura限制而不是程序员的限制?新程序员必须输入每个失败(在大型项目中可能很多),以逐级检查。
爱德华多·科斯塔

5
这不能回答问题。顺便说一句,一些经理在查看承保范围。他们不在乎为什么。他们知道85%优于75%。
ACV

2
测试否则无法访问的代码的一个实际用例是达到100%的测试覆盖率,这样就无需再次检查该类。如果覆盖率停留在95%,许多开发人员可能会试图找出原因,只是一遍又一遍地碰到这个问题。
thisismydesign'2

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
为了使测试具有某种意义,您可能还应该断言构造函数的访问级别就是您期望的级别。
杰里米

加上邪恶的反思,杰里米的想法以及诸如“ testIfConstructorIsPrivateWithoutRaisingExceptions”之类的有意义的名称,我想这就是“ THE”答案。
爱德华多·科斯塔

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(模拟库干扰?)。
David Harkness,2012年

4
我真的很想将其添加到JUnit的Assert类中,但是不想为您的工作而赞誉。我觉得很好。拥有Assert.utilityClassWellDefined()JUnit 4.12+真是太好了。您是否考虑过拉取请求?
有远见的软件解决方案

请注意,setAccessible()用于使构造函数易于访问会导致Sonar的代码覆盖率工具出现问题(当我这样做时,该类将从Sonar的代码覆盖率报告中消失)。
亚当·帕金

谢谢,我确实重设了可访问标志。也许这是声纳本身的错误?
Archimedes Trajano

我查看了Sonar报告以了解我的蜡染maven插件的涵盖范围,它似乎正确涵盖了。 site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano

19

我已经将我的静态实用程序函数的构造函数私有,以满足CheckStyle的要求。但是像原始海报一样,我让Cobertura抱怨测试。最初,我尝试了这种方法,但这不会影响coverage报告,因为构造函数从未真正执行过。因此,实际上所有这些测试都是构造函数是否保持私有状态,并且在后续测试中通过可访问性检查使其变得多余。

@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表示从不调用构造函数,因此不记录覆盖率。
汤姆·麦金太尔

IMO,在此讨论中,第一个代码段中的解决方案是最简洁的。只需与一致即可fail(...)
Piotr Wittchen '17

9

使用Java 8,可以找到其他解决方案。

我假设您只是想用很少的公共静态方法创建实用程序类。如果可以使用Java 8,则可以使用Java 8 interface

package com.XXX;

public interface Foo {

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

没有构造函数,也没有Cobertura的抱怨。现在,您只需要测试您真正关心的行。


1
但是不幸的是,您不能将接口声明为“最终”接口,以防止任何人将其子类化-否则,这将是最好的方法。
Michael Berry

5

测试什么都不做的代码背后的原因是要达到100%的代码覆盖率并注意代码覆盖率何时下降。否则,人们总是会想,嘿,我不再具有100%的代码覆盖率了,但是由于我的私有构造函数,这很可能成为问题。这使得发现未经测试的方法变得容易,而不必检查它只是一个私有的构造函数。随着代码库的增长,您实际上会感到一种温暖的感觉(注视100%而不是99%)。

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具有内置支持,可忽略琐碎的getter / setter / constructor:

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

忽略琐碎的事

忽略琐碎的事情就可以排除包含一行代码的构造函数/方法。一些示例仅包括对超级构造函数的调用,getter / setter方法等。要包括ignore平凡的参数,请添加以下内容:

<cobertura-instrument ignoreTrivial="true" />

或在Gradle构建中:

cobertura {
    coverageIgnoreTrivial = true
}

4

别。测试一个空的构造函数有什么意义?由于cobertura 2.0提供了忽略此类琐碎情况的选项(以及setters / getters),因此可以通过向cobertura maven插件添加配置部分来在maven中启用它:

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

或者,您可以使用Coverage Annotations@CoverageIgnore


3

最后,有解决方案!

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

但是如何测试问题中发布的类?您不应该假定可以将带有私有构造函数的每个类都转换为枚举,也不要想要。
乔恩·斯基特

@JonSkeet我可以上有关课程。大多数实用程序类只有一堆静态方法。否则,只有私有构造函数的类没有任何意义。

1
带有私有构造函数的类可以从公共静态方法实例化,尽管这样很容易获得覆盖。但从根本上讲,我更喜欢可以扩展Enum<E>为真正枚举的任何类……我相信这样可以更好地揭示意图。
乔恩·斯基特

4
哇,我绝对希望代码比任意数字有意义。(覆盖范围并不能保证质量,也不是在所有情况下100%覆盖范围都是可行的。您的测试应充其量指导您的代码-请勿将其引导到怪异的悬崖上。)
Jon Skeet 2012年

1
@Kan:向构造函数添加虚拟调用以使工具虚张声势不是意图。任何依靠单一指标来确定项目福祉的人都已经在走向毁灭之路。
2012年

1

我不了解Cobertura,但我使用Clover,它具有添加模式匹配排除项的方法。例如,我的模式排除了apache-commons-logging行,因此不在覆盖范围内。


1

另一个选择是创建一个类似于以下代码的静态初始化程序

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

  // real ops
}

这种方式认为私有构造函数已经过测试,并且运行时开销基本上是无法测量的。我这样做是为了使用EclEmma获得100%的覆盖率,但可能对每种覆盖率工具都适用。当然,此解决方案的缺点是您仅出于测试目的而编写生产代码(静态初始化程序)。


我做了很多。便宜如便宜,便宜如肮脏,但有效。
pholser 2011年

使用Sonar,实际上会使代码覆盖率完全忽略该类。
亚当·帕金

1

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


这应该是正确的答案,因为它可以准确回答所要提出的问题。
Chakian

0

有时,Cobertura会将不打算执行的代码标记为“未覆盖”,这没有错。您为什么关心99%而不是覆盖100%

从技术上讲,尽管如此,您仍然可以使用反射来调用该构造函数,但这对我来说是非常错误的(在这种情况下)。


0

如果我猜到您的问题的意图,我会说:

  1. 您需要对进行实际工作的私有构造函数进行合理的检查,并且
  2. 您希望三叶草排除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构造函数。例如,在我自己的项目中,我使用类似以下的内容(因为遵循所有Util类的名称都应以Util结尾的约定):

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

我故意省略了.*以下内容,)因为此类构造函数无意引发异常(它们无意做任何事情)。

当然,在第三种情况下,您可能希望为非实用程序类使用空的构造函数。在这种情况下,我建议您将a 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可以防止测试失败,并且可以管理异常,因为这正是您所期望的错误。尽管这是一种解决方法,但我不喜欢“线路覆盖率”与“功能/行为覆盖率”的概念,但我们可以在此测试中找到一种感觉。实际上,您可以确保实用程序类实际上具有一个私有构造函数,该构造函数在通过reflaction调用时也会正确引发异常。希望这可以帮助。


嗨@ShanteshwarInde。非常感谢。我的输入已按照您的建议进行编辑和完成。问候。
Riccardo Solimena

0

我在2019年的首选选项:使用lombok。

具体地,@UtilityClass注释。(在撰写本文时,这只能是“实验性的”,但它的功能还不错,并且前景乐观,因此很可能很快会升级为稳定版。)

此批注将添加私有构造函数以防止实例化,并使类最终化。当与lombok.addLombokGeneratedAnnotation = truein 结合使用时lombok.config,几乎所有测试框架在计算测试覆盖率时都将忽略自动生成的代码,从而使您能够绕开自动生成的代码的覆盖范围,而不会受到黑手或反思。


-2

你不能

您显然正在创建私有构造函数,以防止实例化仅包含静态方法的类。与其尝试覆盖此构造函数(这将要求实例化该类),不如摆脱它,并信任您的开发人员不要向该类添加实例方法。


3
那是不对的。您可以通过反射实例化它,如上所述。
theotherian

不好,永远不要让默认的公共构造函数出现,您应该添加私有的以防止调用它。
Lho Ben
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.