use*_*515 5 java tdd class-visibility
在某些方法中使用的java类中有一个私有字符串常量,当我想对这个类进行测试时,我必须在测试中对字符串常量进行硬编码,因为它在类中是私有的。但是如果将来在类中修改字符串常量,我也必须在测试类中修改。我不想仅仅为了测试而修改这个常量的可见性,以避免将来可能发生这种修改。根据 TDD 方法论,这种想法是否正确?
谢谢。
Mar*_*ann 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);
}
Run Code Online (Sandbox Code Playgroud)
这可能会推动这样的实现:
public sealed class Greeter
{
private const string englishTemplate = "Hello, {0}.";
public string Greet(string name)
{
return string.Format(englishTemplate, name);
}
}
Run Code Online (Sandbox Code Playgroud)
与 OP 中一样,此实现包含一个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);
}
Run Code Online (Sandbox Code Playgroud)
这里的第一个断言验证了name可以在 中的某处找到actual。第二个断言阻止实现简单地回显名称。
现在,如果您更改模板,这个强大的测试仍然可以通过,而第一个示例将会失败。
现在,我确信您在问:但是我如何验证返回值不只是一些废话?
从某种意义上说,您不能,因为您已经决定实际价值不再是合同的一部分。想象一下,您被要求国际化 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);
}
Run Code Online (Sandbox Code Playgroud)
这可能会推动这样的实现:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
再次强调,这是一个精确的规范。我会提醒您,这个示例只是“真实”问题的占位符。如果确切的字符串是规范的一部分,那么按上面所示编写测试似乎是合适的,但这也意味着如果更改字符串,测试将会中断。这是设计使然。
如果您想编写更强大的测试,您可以这样做:
[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);
}
Run Code Online (Sandbox Code Playgroud)
这些断言再次验证该名称是否包含在响应中,而不仅仅是回显。此外,测试断言丹麦语问候语与英语问候语不同。
该测试比确切的规范更可靠,但不能绝对保证永远不会失败。如果将来翻译人员决定英语问候语和丹麦语问候语应使用有效相同的模板,则测试将会失败。
回到OP。将模板公开为公共常量怎么样?
当然,如果确切的值不是规范的一部分,您可以这样做:
public const string EnglishTemplate = "Hello, {0}.";
public const string DanishTemplate = "Hej {0}.";
Run Code Online (Sandbox Code Playgroud)
这将使您能够编写如下测试:
[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);
}
Run Code Online (Sandbox Code Playgroud)
然而,这有什么价值吗?它本质上只是重复实现代码。
不要重复自己。
不过,只是为了说明这一点:这样的测试对于模板字符串的更改来说是稳健的。即使您编辑字符串,测试也会通过。这样合适吗?
这取决于确切的字符串值是否是 SUT 公共合同的一部分。
这种推理不仅适用于字符串,也适用于常数。
如果常量是合同的一部分,请以这样的方式编写测试:如果常量发生变化,测试就会失败。如果常量是实现细节,请编写在值更改时通过的健壮测试。
将测试的可见性更改为包私有应该没问题。尽管如此,它仍然可以在同一个包中使用,如果其他开发人员不知道他不应该这样做。如果您稍后更改该值,这可能会破坏某些内容。
更好的方法是使用一些抽象来获取这个值,而不是直接使用它。例如:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
现在您可以valueSupplier在单元测试中进行模拟。SOME_VALUE这允许您随心所欲地进行更改,而不会破坏现有测试,同时仍保持其私密性。