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

这是密码:

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

96564 次浏览

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

如果有一个注释或者类似的东西可以添加到类中,使 Cobertura 明白它不会被调用,那么就这样做: 我不认为人为地添加覆盖值是值得的。

编辑: 如果没有办法做到这一点,只是生活与轻微减少的覆盖面。请记住,覆盖是有用的东西是 -你应该负责的工具,而不是反过来。

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

但是,从技术上讲,您仍然可以使用反射调用该构造函数,但是在我看来(在本例中)这是非常错误的。

你不能。

显然,您创建私有构造函数是为了防止只包含静态方法的类的实例化。与其试图覆盖这个构造函数(这需要类被实例化) ,您应该去掉它,并且相信您的开发人员不会将实例方法添加到类中。

我不知道 Cobertura,但我使用三叶草,它有一种添加模式匹配排除的方法。例如,我有一些排除 apache-commons-log 行的模式,这样它们就不会计入覆盖范围。

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

另一种选择是创建一个类似于以下代码的静态初始值设定项

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


// real ops
}

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

测试代码不做任何事情背后的原因是为了实现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());
}
}
And then just add classes to the array as you go.

终于有办法了!

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

为了满足 CheckStyle,我将静态实用函数类的构造函数设置为 private。但就像最初的海报一样,我让 Cobertura 抱怨测试。起初我尝试了这种方法,但是这并不影响覆盖率报告,因为构造函数实际上从未执行过。因此,实际上所有这些测试都是在构造函数是否保持私有的情况下进行的——在后续测试中的可访问性检查会使这一点变得多余。

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

我采纳了 Javid Jamae 的建议,使用了反射,但是添加了断言来捕捉任何扰乱正在测试的类的人(并将测试命名为表示“高级别的邪恶”)。

@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% 的方法覆盖率。

虽然这不一定是为了覆盖,但是我创建这个方法是为了验证实用工具类是否定义良好,以及是否也进行了一些覆盖。

/**
* 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. 您希望对执行实际工作的私有构造函数进行合理的检查,并且
  2. 您希望三叶草排除 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 构造函数,如果您有一个设置命名模式为 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>
@Test
public void testTestPrivateConstructor() {
Constructor<Test> cnt;
try {
cnt = Test.class.getDeclaredConstructor();
cnt.setAccessible(true);


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

Java 是您的源文件,它具有您的私有构造函数

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

我假设您只是希望创建具有少量公共静态方法的实用程序类。如果可以使用 Java8,那么可以改用 interface

package com.XXX;


public interface Foo {


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

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

Cobertura 的新版本内置了忽略琐碎的 getter/setter/构造函数的支持:

Https://github.com/cobertura/cobertura/wiki/ant-task-reference#ignore-trivial

忽略琐事

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

<cobertura-instrument ignoreTrivial="true" />

或者是格拉德尔式的:

cobertura {
coverageIgnoreTrivial = true
}

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

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

或者,您可以使用 覆盖范围注释: @CoverageIgnore

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

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

具体来说,就是 @UtilityClass注释。(遗憾的是,在撰写本文时,它只是“试验性”的,但功能正常,前景乐观,所以很可能很快就会升级到稳定状态。)

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

下面这个用龙目岛注释@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
}

尽管在手动编写 private structor 时,structor.setAccability (true)应该可以工作,但是龙目岛注释不能工作,因为它是强制性的。 NewInstance ()实际上测试构造函数是否被调用,这就完成了构造函数本身的覆盖范围。使用 assertThrows 可以防止测试失败,并管理异常,因为这正是您所预期的错误。 虽然这是一个变通方法,我不喜欢“行覆盖率”和“功能/行为覆盖率”的概念,但是我们可以在这个测试中找到一些意义。 事实上,您可以确定 UtilityClass 实际上有一个私有构造函数,当通过反射调用时,该构造函数可以正确地引发异常。 希望这个能帮上忙。