将一个私有Java常量的可见性更改为测试它。

5
在Java类中有一个私有的String常量被用于一些方法,当我想要对该类进行测试时,由于其在类中是私有的,我将不得不在测试中硬编码该字符串常量。但是如果将来在类中修改了这个字符串常量,我也必须在测试类中进行修改。为了避免将来的可能性修改,我不想修改此常量的可见性仅供测试使用。根据TDD方法论,这种想法是否正确?
谢谢。

对不起,你必须要修改这个常量的可见性以避免未来可能的修改,我不想仅仅为了测试而修改它的可见性。 - Louis Wasserman
2个回答

18

由于这个问题被标记为tdd,我会假装OP没有包含

当我想要为这个类编写测试时

毕竟,TDD意味着测试驱动开发;即在实现之前编写测试。

此外,由于这个问题是几种编程语言的问题,而不仅仅是Java,我将用一些C#代码来回答。我相信你能够理解它。

然而,简短的答案是取决于该常量是否是系统测试(SUT)规范的一部分。

想象一下,作为一个说明性的例子,你必须TDD一个小的Hello World API。

你可以写几个测试用例,如下所示:

[Theory]
[InlineData("Mary")]
[InlineData("Sue")]
public void ExactEnglishSpec(string name)
{
    var sut = new Greeter();

    var actual = sut.Greet(name);

    Assert.StartsWith("Hello, ", actual);
    Assert.EndsWith(name + ".", actual);
}

这可能会驱动这样的实现:
public sealed class Greeter
{
    private const string englishTemplate = "Hello, {0}.";

    public string Greet(string name)
    {
        return string.Format(englishTemplate, name);
    }
}

就像在原帖中一样,这个实现包含一个private const string。如果那个englishTemplate改变了会发生什么?

显然,测试会失败。

因为你是按照这种方式编写的。

测试规定了软件的行为,如果它们断言一个字符串应该恰好是特定的值,那么这是规范的一部分,如果它改变了,那么这是一个破坏性的变化。

这可能是你想要的,也可能不是。

这取决于你在做什么。如果你正在实现一个正式的规范,要求确切的模板字符串,那么它应该是合同的一部分,如果有人改变了模板,它应该是一个破坏性的变化。如果它是基于该模板的API的一部分,并且你已经与调用者达成了协议,那么这也是正确的。

另一方面,这可能不是你想要的。如果你正在实现一个(面向对象的)API来封装行为,并且你期望模板将来可能会改变,那么请编写更强大的测试:

[Theory]
[InlineData("Rose")]
[InlineData("Mary")]
public void RobustEnglishBehaviour(string name)
{
    var sut = new Greeter();

    var actual = sut.Greet(name);

    Assert.Contains(name, actual);
    Assert.NotEqual(name, actual);
}

这里的第一个断言验证了nameactual中可以找到。第二个断言防止实现简单地回显名称。

现在,如果您更改模板,此强大的测试仍将通过,而第一个示例将会失败。

到目前为止,我相信您会问:但是我如何验证返回值不仅仅是一些无意义的东西呢?

从某种意义上说,您不能,因为您已经决定实际值不再是合同的一部分。想象一下,您被要求国际化API,而不是英语问候语。作为开发人员,您能够验证罗马尼亚语、中文、泰语、韩语等问候语是否正确吗?

可能不能。这些字符串对您来说将是不透明的,希望由专业翻译提供。

那么,您如何针对这样的内容进行测试呢?

天真地说,有人可能会尝试这样做:

[Theory]
[InlineData("Hans")]
[InlineData("Christian")]
public void ExactDanishSpec(string name)
{
    var sut = new Greeter();
    sut.Culture = "da-DK";

    var actual = sut.Greet(name);

    Assert.StartsWith("Hej ", actual);
    Assert.EndsWith(name + ".", actual);
}

这可能会驱动这样的实现:

public sealed class Greeter
{
    private const string englishTemplate = "Hello, {0}.";
    private const string danishTemplate = "Hej {0}.";

    public string? Culture { get; set; }

    public string Greet(string name)
    {
        if (Culture == "da-DK")
            return string.Format(danishTemplate, name);
        else
            return string.Format(englishTemplate, name);
    }
}

再次强调,这是一个精确的规范。我提醒您,此示例仅用作“真实”问题的占位符。如果确切的字符串是规范的一部分,则编写如上所示的测试似乎是合适的,但这也意味着如果您更改字符串,则测试将会失败。这是有意设计的。

如果您想编写更健壮的测试,可以尝试以下方法:

[Theory]
[InlineData("Charlotte")]
[InlineData("Martin")]
public void EnglishIsDifferentFromDanish(string name)
{
    var sut = new Greeter();
    var unexpected = sut.Greet(name);
    sut.Culture = "da-DK";

    var actual = sut.Greet(name);

    Assert.Contains(name, actual);
    Assert.NotEqual(name, actual);
    Assert.NotEqual(unexpected, actual);
}

这些断言再次验证了名称包含在响应中,而不仅仅是回显。此外,测试断言丹麦语问候语与英语问候语不同。

这个测试比确切规范更加健壮,但并不能绝对保证永远不会失败。如果将来翻译人员决定英语问候语和丹麦语问候语应使用有效相等的模板,则测试将失败。

回到OP。将模板公开为公共常量怎么样?

当确切值不是规范的一部分时,您可以这样做:

public const string EnglishTemplate = "Hello, {0}.";
public const string DanishTemplate = "Hej {0}.";

这将使您能够编写如下的测试:

这将使您能够编写像这样的测试:

[Theory]
[InlineData("Thelma")]
[InlineData("Louise")]
public void UseEnglishTemplate(string name)
{
    var sut = new Greeter();
    var actual = sut.Greet(name);
    Assert.Equal(string.Format(Greeter.EnglishTemplate, name), actual);
}

然而,这是否提供任何价值?它本质上只是重复实现代码。

不要重复自己。

尽管如此,为了强调这一点:这样的测试将对模板字符串的更改具有鲁棒性。即使您编辑了字符串,测试也会通过。这合适吗?

这取决于确切的字符串值是否是SUT的公共合同的一部分。

这种推理方式不仅适用于字符串,还适用于常量数字。

如果常量是合同的一部分,请编写测试,以便在常量更改时失败。如果常量是实现细节,请编写鲁棒的测试,在值更改时通过。


我经常遇到这个问题,即开发人员通过断言(内部)错误消息的相等性来测试他们的异常;而我几乎无法表达“是规范的一部分”还是不是这种区别。任何明确清晰地表明这种区别的书面说明似乎都与代码本身没有什么区别。 - jaco0646

0

将测试的可见性更改为包私有应该是可以的。但是,如果其他开发人员不知道他不应该这样做,它可能会在相同的包中使用。如果以后更改了值,则可能会破坏某些东西。

更好的方法是使用一些抽象来获取此值,而不是直接使用它。例如:

public class MyClass {

  private static final String SOME_VALUE = "value";

  private final Supplier<String> valueSupplier;

  public MyClass(Supplier<String> valueSupplier) {
    //new constructor
    this.valueSupplier = valueSupplier;
  }

  public MyClass() {
    //previous constructor
    //provides the default supplier with the constant
    this(() -> SOME_VALUE);
  }

  public String doStuff(String stringValue) {
    //method to test
    return this.valueSupplier.get() + "_" + stringValue;
  }
}

现在你可以在单元测试中模拟valueSupplier。这使你能够随意更改SOME_VALUE,而不会破坏现有的测试,同时仍然保持其私密性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接