【问题标题】:How to add test coverage to a private constructor?如何将测试覆盖率添加到私有构造函数?
【发布时间】:2011-05-30 01:05:09
【问题描述】:

这是代码:

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 说类的私有构造函数的代码覆盖率为零。我们如何为这样的私有构造函数添加测试覆盖率?

【问题讨论】:

  • 在我看来,好像您正在尝试强制执行单例模式。如果是这样,您可能会喜欢 dp4j.com(正是这样做的)
  • 不应该用抛出异常来替换“故意为空”吗?在这种情况下,您可以编写测试来期望带有特定消息的特定异常,不是吗?不确定这是否矫枉过正

标签: java testing code-coverage


【解决方案1】:

我并不完全同意 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();
}

【讨论】:

  • 但这是通过向测试套件添加噪音来消除覆盖率报告中的噪音。我会以“把理想主义放在一边”结束这句话。 :)
  • 为了给这个测试赋予任何意义,你可能还应该断言构造函数的访问级别是你所期望的。
  • 这在语法上是不正确的,不是吗? constructor 是什么? Constructor 不应该被参数化而不是原始类型吗?
  • 这是错误的:constructor.isAccessible() 总是返回 false,即使在公共构造函数上也是如此。应该使用assertTrue(Modifier.isPrivate(constructor.getModifiers()));
  • 如果你使用 JaCoCo,>= 0.7.10 will ignore empty private constructors
【解决方案2】:

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

如果您可以在类中添加注释或任何类似内容以使 Cobertura 了解它不会被调用,请执行此操作:我认为人为地添加覆盖范围是不值得的。

编辑:如果没有办法做到这一点,那就忍受稍微减少的覆盖范围。请记住,覆盖意味着对有用 - 您应该负责该工具,而不是相反。

【讨论】:

  • 我不想仅仅因为这个特殊的构造函数而在整个项目中“稍微减少覆盖率”..
  • @Vincenzo:那么 IMO 你对一个简单数字的评价太高了。覆盖率是测试的指标。不要成为工具的奴隶。覆盖的重点是给您一定程度的信心,并建议进行额外测试的区域。人为地调用一个未使用的构造函数对这两点都没有帮助。
  • @JonSkeet:我完全同意“不要成为工具的奴隶”,但是记住每个项目中的每个“缺陷计数”并不好。如何确保 7/9 结果是 Cobertura 限制,而不是程序员的?新程序员必须输入每个失败(在大型项目中可能很多)以逐类检查。
  • 这没有回答问题。顺便说一句,一些经理会查看覆盖率。他们不在乎为什么。他们知道 85% 比 75% 好。
  • @ACV:那么他们“知道”的是不正确的。如果您有两组测试覆盖相同的代码,则无法保证覆盖率较高的测试实际上是更好的测试。我会毫不犹豫地与一位经理争论,他开始应用可疑的质量指标而不了解它们。
【解决方案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

【讨论】:

  • +1 这不仅在不欺骗工具的情况下解决了问题,而且完全测试了设置实用程序类的编码标准。我不得不更改可访问性测试以使用Modifier.isPrivate,因为isAccessible 在某些情况下为私有构造函数返回true(模拟库干扰?)。
  • 我真的很想把它添加到 JUnit 的 Assert 类中,但不想因为你的工作而受到赞扬。我认为这非常好。在 JUnit 4.12+ 中有 Assert.utilityClassWellDefined() 会很棒。你考虑过拉取请求吗?
  • 请注意,使用 setAccessible() 使构造函数可访问会导致 Sonar 的代码覆盖工具出现问题(当我这样做时,该类会从 Sonar 的代码覆盖报告中消失)。
  • 谢谢,不过我确实重置了可访问标志。也许这是声纳本身的错误?
  • 我查看了我的 Sonar 报告以了解我的 batik maven 插件的覆盖范围,它似乎覆盖正确。 site.trajano.net/batik-maven-plugin/cobertura/index.html
【解决方案4】:

我已经将我的静态实用函数类的构造函数设为私有,以满足 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% 方法覆盖的温暖模糊感。

【讨论】:

  • 它可能是矫枉过正,但如果它是在 Unitils 或类似的,我会使用它
  • +1 好的开始,虽然我选择了Archimedes's more complete test
  • 第一个示例不起作用 - IllegalAccesException 表示从未调用过构造函数,因此不会记录覆盖范围。
  • IMO,第一个代码 sn-p 中的解决方案是本次讨论中最干净和最简单的解决方案。不需要与fail(...) 对齐。
【解决方案5】:

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

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

package com.XXX;

public interface Foo {

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

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

【讨论】:

  • 不幸的是,您不能将接口声明为“final”,从而阻止任何人对其进行子类化——否则这将是最好的方法。
  • 因为子类化会让你访问已经公开的方法?当它实际上是一个很好的解决方案时,我们不要试图证明一切都是白痴。
【解决方案6】:

测试不做任何事情的代码背后的原因是为了实现 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());
        }
    }
然后你就可以在数组中添加类了。

【讨论】:

    【解决方案7】:

    较新版本的 Cobertura 内置支持忽略琐碎的 getter/setter/constructors:

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

    忽略琐碎

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

    <cobertura-instrument ignoreTrivial="true" />
    

    或在 Gradle 构建中:

    cobertura {
        coverageIgnoreTrivial = true
    }
    

    【讨论】:

      【解决方案8】:

      不要。 测试一个空的构造函数有什么意义? 由于 cobertura 2.0 有一个选项可以忽略这些琐碎的情况(连同 setter/getter),您可以通过向 cobertura maven 插件添加配置部分来在 maven 中启用它:

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

      您也可以使用Coverage Annotations:@CoverageIgnore

      【讨论】:

        【解决方案9】:

        终于有办法了!

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

        【讨论】:

        • 如何测试问题中发布的课程?您不应该假设您可以将具有私有构造函数的每个类都转换为枚举,或者您希望这样做。
        • @JonSkeet 我可以参加有问题的课程。大多数实用程序类只有一堆静态方法。否则只有私有构造函数的类没有任何意义。
        • 具有私有构造函数的类可以从公共静态方法中实例化,当然这样很容易获得覆盖。但从根本上说,我希望任何扩展 Enum&lt;E&gt; 的类真正成为一个枚举......我相信这能更好地揭示意图。
        • 哇,我绝对更喜欢比任意数字更有意义的代码。 (覆盖率并不能保证质量,100% 的覆盖率也不是在所有情况下都是可行的。您的测试充其量应该指导您的代码 - 而不是将其引导到奇怪意图的悬崖上。)
        • @Kan:向构造函数添加一个虚拟调用来虚张声势的工具不应该是本意。任何依赖单一指标来确定项目健康状况的人都已经走上了毁灭之路。
        【解决方案10】:

        以下内容对我使用 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 可以防止测试失败并管理异常,因为这正是您期望的错误。 尽管这是一种解决方法,而且我不理解“线路覆盖率”与“功能/行为覆盖率”的概念,但我们可以在这个测试中找到意义。 事实上,您确信实用程序类实际上有一个私有构造函数,它在通过反射调用时也会正确抛出异常。 希望这会有所帮助。

        【讨论】:

        • 嗨@ShanteshwarInde。非常感谢。我的输入已根据您的建议进行了编辑和完成。问候。
        【解决方案11】:

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

        具体来说,@UtilityClass annotation。 (遗憾的是,在撰写本文时只是“实验性”,但它运行良好且前景乐观,因此可能很快会升级为稳定版。)

        此注解将添加私有构造函数以防止实例化并使类成为最终类。当与lombok.config 中的lombok.addLombokGeneratedAnnotation = true 结合使用时,几乎所有测试框架都会在计算测试覆盖率时忽略自动生成的代码,从而允许您绕过自动生成的代码的覆盖率,而无需进行修改或反射。

        【讨论】:

          【解决方案12】:

          我不了解 Cobertura,但我使用 Clover,它可以添加模式匹配排除项。例如,我的模式排除了 apache-commons-logging 行,因此它们不计入覆盖范围。

          【讨论】:

            【解决方案13】:

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

            class YourClass {
              private YourClass() {
              }
              static {
                 new YourClass();
              }
            
              // real ops
            }
            

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

            【讨论】:

            • 我经常这样做。物美价廉,物美价廉,但有效。
            • 使用 Sonar,这实际上会导致类完全被代码覆盖所遗漏。
            【解决方案14】:

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

            【讨论】:

            • 这应该是正确的答案,因为它准确地回答了所问的问题。
            【解决方案15】:

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

            不过,从技术上讲,您仍然可以通过反射调用该构造函数,但对我来说这听起来很不对(在这种情况下)。

            【讨论】:

              【解决方案16】:

              如果我猜测你的问题的意图,我会说:

              1. 您希望对执行实际工作的私有构造函数进行合理检查,并且
              2. 您希望 clover 排除 util 类的空构造函数。

              对于 1,很明显您希望所有初始化都通过工厂方法完成。在这种情况下,您的测试应该能够测试构造函数的副作用。这应该属于普通私有方法测试的范畴。使方法更小,以便它们只做有限数量的确定的事情(理想情况下,只做一件事,做好一件事),然后测试依赖它们的方法。

              例如,如果我的 [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 类设置了命名模式,则可以配置 clover 以排除 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>
              

              【讨论】:

                【解决方案17】:
                @Test
                public void testTestPrivateConstructor() {
                    Constructor<Test> cnt;
                    try {
                        cnt = Test.class.getDeclaredConstructor();
                        cnt.setAccessible(true);
                
                        cnt.newInstance();
                    } catch (Exception e) {
                        e.getMessage();
                    }
                }
                

                Test.java 是你的源文件,它有你的私有构造函数

                【讨论】:

                • 很高兴解释一下为什么这个结构有助于覆盖。
                • 是的,其次:为什么要在测试中捕获异常?抛出的异常实际上应该使测试失败。
                【解决方案18】:

                你不能。

                您显然是在创建私有构造函数以防止实例化仅包含静态方法的类。您应该摆脱它并相信您的开发人员不会向该类添加实例方法,而不是试图覆盖此构造函数(这将需要实例化类)。

                【讨论】:

                • 不正确;如上所述,您可以通过反射来实例化它。
                • 那很糟糕,永远不要让默认的公共构造函数出现,你应该添加私有构造函数以防止调用它。
                猜你喜欢
                • 2018-07-08
                • 2015-11-30
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2014-04-11
                • 2016-05-24
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多