【问题标题】:Unit testing code that relies on constant values依赖于常量值的单元测试代码
【发布时间】:2015-11-23 04:49:32
【问题描述】:

考虑以下(完全人为的)示例:

public class Length {
    private static final int MAX_LENGTH = 10;
    private final int length;
    public Length(int length) {
        if (length > MAX_LENGTH)
            throw new IllegalArgumentException("Length too long");
        this.length = length;
    }
}

我想测试一下,当以大于MAX_LENGTH 的长度调用它时会引发异常。有多种测试方法,都有缺点:

@Test(expected = IllegalArgumentException.class)
public void testMaxLength() {
    new Length(11);
}

这复制了测试用例中的常量。如果MAX_LENGTH 变小,这将不再是边缘情况(尽管显然它应该与单独的情况配对以测试边缘的另一侧)。如果它变得更大,这将失败并需要手动更改(这可能不是一件坏事)。

这些缺点可以通过为MAX_LENGTH 添加一个getter 来避免,然后将测试更改为:

new Length(Length.getMaxLength());

这似乎好多了,因为如果常量发生变化,则不需要更改测试。另一方面,它暴露了一个本来是私有的常量,并且它具有同时测试两种方法的重大缺陷 - 如果两种方法都被破坏,测试可能会给出误报。

另一种方法是根本不使用常量,而是注入依赖项:

interface MaxLength {
    int getMaxLength();
}

public class Length {
    public static void setMaxLength(MaxLength maxLength);
}

然后可以将“常量”作为测试的一部分进行模拟(此处使用 Mockito 的示例):

MaxLength mockedLength = mock(MaxLength.class);
when(mokedLength.getMaxLength()).thenReturn(17);
Length.setMaxLength(mockedLength);
new Length(18);

这似乎增加了很多复杂性而不是很多价值(假设没有其他理由注入依赖项)。

在这个阶段,我的偏好是使用第二种公开常量的方法,而不是硬编码测试中的值。但这对我来说似乎并不理想。有更好的选择吗?还是这些案例缺乏可测试性表明存在设计缺陷?

【问题讨论】:

  • 单元测试的目的是在您的软件不符合您定义的规范时发出警报。如果您的 MAX_LENGTH 常量由于某种原因突然减少,我认为您的测试用例中断是一件好事。换句话说,测试用例的目的不仅仅是让某些东西通过,而是检测代码中的异常情况。如果您不同意我的想法,请加入。
  • 如果 MAX_LENGTH 是 publicprotected,则单元测试可以使用 MAX_LENGTH+1。允许更多地访问您的代码以简化单元测试是一个备受争议的主题。 IMO 对常量执行此操作是可以的,但是 YMMV。
  • @user949300 我建议使用 package-private,而不是 publicprotected,这样只有同一个包中的代码才能看到它,并且测试类应该在同一个包(虽然不同的源文件夹)。但是我更同意 Tim 的评论,即保留 private,如果实现随着测试代码的相应更改而发生更改,则让测试失败,这是应该的。
  • @Andreas 很好的建议,如果在我的手机上输入“package-protected”不是那么烦人的话,我会添加的。 Java 的某些错误之一是将其用作默认可见性。

标签: java unit-testing junit mockito


【解决方案1】:

正如Tim 在 cmets 中提到的,您的目标是确保您的软件按照规范运行。一种这样的规范可能是最大长度始终为 10,此时无需测试长度为 5 或 15 的世界。

这是要问自己的问题:您希望使用具有不同“常量”值的类的可能性有多大?我在这里引用了“常量”,因为如果你以编程方式改变值,它根本不是一个常数,是吗? :)

  • 如果您的值永远不会改变,则根本不能使用符号常量,只需直接与 10 比较并基于(例如)0、3、10 和11. 这可能会让你的代码和测试有点难以理解(“10 是从哪里来的?11 是从哪里来的?”),如果你确实有理由改变数字。不推荐。

  • 如果你的值可能永远不会改变,你可以像你一样使用一个私有的命名常量(即一个静态的 final 字段)。这样您的代码将很容易更改,尽管您的测试将无法自动调整代码的方式。

    • 您还可以放松到包私有可见性,这将可用于同一包中的测试。 Javadoc(例如/** Package-private for testing. */)或文档注释(例如@VisibleForTesting)可能有助于明确您的意图。如果您的常量值旨在不透明且在您的类之外不可用,例如 URL 模板或身份验证令牌,这是一个不错的选择。

    • 您甚至可以将其设为公共常量,您的类的消费者也可以使用它。 对于您的常量长度示例,公共静态最终字段可能是最好的,假设您的系统的其他部分可能想知道这一点(例如,对于 UI 验证提示或错误消息)。

  • 如果您的值可能会改变,您可以按实例接受它,如new Length(10)new Length().setMaxLength(10)。 (我认为前者是依赖注入的一种形式,将常量整数计算为依赖项。)如果您想在测试中使用不同的值,这也是一个好主意,例如在生产中使用最大长度 2048 但为实用起见,针对 10 进行测试。 为了制作一个灵活的长度验证器,这个选项可能是对静态最终字段的一个很好的升级。

  • 只有当您的值可能在实例的生命周期内发生变化时,我才会使用 DI 样式的值提供程序。此时,您可以交互地查询该值,因此它的行为根本不像一个常量。对于“长度”,这显然是矫枉过正,但对于“最大允许内存”、“最大同时连接数”或其他类似的伪常数,可能不是。

简而言之,你必须决定你需要多少控制,然后你可以从中选择最直接的选择;作为“默认”,您可能希望将其设为可见字段或构造函数参数,因为它们往往具有简单性和灵活性的良好平衡。

【讨论】:

  • 感谢杰夫的建议。在我的情况下,听起来最明智的答案就是让测试(在同一个包中)可以看到常量并在测试用例中使用它。再次感谢您的想法。
猜你喜欢
  • 1970-01-01
  • 2014-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多