常量是美丽的人 - 他们可以在一个独特的地方保存一个在代码中无处不在的值。更改该值只需要一个简单的修改。
生活很酷。
嗯,这是承诺。现实有点不同:
LogCompleteFileName
常量值从L:\LOGS\MyApp.log
更改为\\Traces\App208.txt
,您会得到两个文件:跟踪的\\traces\App208.txt
和日志的\\traces\App208.txt.log
。 TransactionTimeout
从2分钟更改为4分钟,并且在2分钟后仍然会超时(在花了一天之后,您会发现您还必须更改DBMS的超时和事务超时管理器...)。SleepTimeInMinutes
从1
替换为10
并且您看不到任何更改(大约一小时后,您会发现该常量的名称具有误导性:粒度不是分钟但毫秒......)。CompanyName
更改为Yahoo
更改为Microsoft
,但自动邮件提醒仍会发送至alert@yahoo.com
... 创建常量是契约。你告诉你的读者,只要他们改变了价值,它仍然会按照他们认为应该的方式运作。
没什么。
当然,您需要测试一下您是否误导了您的读者。你必须确保隐含的合同是正确的。
您如何通过TDD实现这一目标?我只是坚持这一点。我可以测试常量(!)值的变化的唯一方法是使应用程序设置保持不变...当我认为值可以和时,我必须得出结论:const
关键字应该被避免会改变吗?
您如何使用TDD测试您的(所谓的)常量?
非常感谢提前:)
答案 0 :(得分:12)
我可以测试常量(!)值的变化的唯一方法是使该常量变为应用程序设置
您在问题中列出的所有用法听起来都像应用程序设置,而不是常量。常量是一个恒定的值,例如:
const decimal LITERS_PER_HOGSHEAD = 238.480942392;
编辑补充:希望这比我轻率的答案更有帮助。我通常创建一个AppSettings类。这个类中的一些属性是从配置文件中提取的,有些是我不希望改变的设置,有些可能是常量。
public class AppSettings
{
public const decimal GILLS_PER_HOMER = 1859.771248601;
public string HelpdeskPhone
{
get { // pulled from config and cached at startup }
}
public int MaxNumberOfItemsInAComboBox
{
get { return 3; }
}
}
答案 1 :(得分:5)
有两种常数:
使用TDD编写代码时,每行生产代码都应该存在,因为首先存在需要编写代码的失败测试。当您重构代码时,一些神奇的值将被提升为常量。其中一些可能也适用于应用程序设置,但为方便起见(代码较少),它们已在代码中配置而不是外部配置文件。
在这种情况下,我编写测试的方式,生产和测试代码都将使用相同的常量。测试将指定常量按预期使用。但是测试不会重复常量的值,例如在“assert MAX_ITEMS == 4
”中,因为这将是重复的代码。相反,测试将检查某些行为是否正确使用常量。
例如,here是一个示例应用程序(由我编写),它将打印指定长度的Longcat。如您所见,Longcat被定义为一系列常量:
public class Longcat {
// Source: http://encyclopediadramatica.com/Longcat
public static final String HEAD_LINES = "" +
" /\\___/\\ \n" +
" / \\ \n" +
" | # # | \n" +
" \\ @ | \n" +
" \\ _|_ / \n" +
" / \\______ \n" +
" / _______ ___ \\ \n" +
" |_____ \\ \\__/ \n" +
" | \\__/ \n";
public static final String BODY_LINE = "" +
" | | \n";
public static final String FEET_LINES = "" +
" / \\ \n" +
" / ____ \\ \n" +
" | / \\ | \n" +
" | | | | \n" +
" / | | \\ \n" +
" \\__/ \\__/ \n";
...
测试验证常量是否正确使用,但它们不会复制常量的值。如果我更改常量的值,则所有测试都将自动使用新值。 (无论Longcat ASCII艺术看起来是否合适,都需要手动验证。虽然你甚至可以通过验收测试自动化,这对于更大的项目来说是值得推荐的。)
public void test__Longcat_with_body_size_2() {
Longcat longcat = factory.createLongcat(2);
assertEquals(Longcat.BODY_LINE + Longcat.BODY_LINE, longcat.getBody());
}
public void test__Fully_assembled_longcat() {
Longcat longcat = factory.createLongcat(2);
assertEquals(Longcat.HEAD_LINES + longcat.getBody() + Longcat.FEET_LINES, longcat.toString());
}
同一个应用程序还有一些永远不会改变的常量,例如米和英尺之间的比例。这些值可以/应该硬编码到测试中:
public void test__Identity_conversion() {
int feet1 = 10000;
int feet2 = FEET.from(feet1, FEET);
assertEquals(feet1, feet2);
}
public void test__Convert_feet_to_meters() {
int feet = 10000;
int meters = METERS.from(feet, FEET);
assertEquals(3048, meters);
}
public void test__Convert_meters_to_feet() {
int meters = 3048;
int feet = FEET.from(meters, METERS);
assertEquals(10000, feet);
}
生产代码如下:
public enum LengthUnit {
METERS("m", 1.0), FEET("ft", 0.3048), PETRONAS("petronas", 451.9), LINES("lines", 0.009);
private final String name;
private final double lengthInMeters;
...
public int from(int length, LengthUnit unit) {
return (int) (length * unit.lengthInMeters / this.lengthInMeters);
}
}
请注意,我没有为Petronas Twin Towers的高度编写任何测试,因为该信息是声明性的(并且声明性数据很少被破坏)并且我已经为转换逻辑编写了测试(参见上文)。如果添加更多类似的常量(艾菲尔铁塔,帝国大厦等),它们将由应用程序自动定位,并将按预期工作。
答案 2 :(得分:3)
从我读到的你的问题来看,这与TDD无关。您描述的用法不是真正的常量,而是配置值,因此在这些情况下,您不应使用const
修饰符。
答案 3 :(得分:3)
那是因为所有这些事情都不是常数......事实上是:
如果这些事情发生变化,请不要担心,您的应用程序崩溃将是最后一个重要的事情xD
答案 4 :(得分:1)
有几件事。
首先TDD和TDD推出的紧急设计是分离你的责任,应用DRY和依赖注入。
对单元测试const很容易,也许有点无意义。
但是,测试另一个单位对该const的评估在我看来不是单元测试。这是集成测试。在其他单元中测试const值将由模拟或存根覆盖。
其次你的例子是多种多样的:
您的日志示例仅使用1个文件。它只是存在2。如果要求只存在一个文件,那么你可以为此制作一个测试。
交易超时测试应该通过集成测试来获取。它应该表明您的原始测试不是问题所在。
更改CompanyName是好的,因为它与公司名称有关。域名应该并且可以是不同的const。
正如其他人提到的那样,在测试其他类时,传递一个配置类对mock / stub很有帮助。
答案 5 :(得分:0)
听起来像你使用常量主要是为了说明配置设置。这对于ConfigurationManager来说是理想的,但这也很难用于测试。
我建议使用以下用法:
const String SomeValue = "TESTCONSTANT";
static class ConfigurationSettings
{
static String SomeProperty
{
get
{
var result = SomeValue;
if (ConfigurationManager.AppSettings["SOMEKEY"] != null)
result = ConfigurationManager.AppSettings["SOMEKEY"];
return result;
}
}
}
答案 6 :(得分:0)
如果你正在进行单元测试,我不相信这样的事情有什么问题:
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void DefaultValue_Equals_8()
{
Assert.AreEqual<int>(8, MyNamespace.MyClass.DefaultValue);
}
测试:
namespace MyNamespace
{
public class MyClass
{
public const int DefaultValue = 8;
}
}
这将检测常数是否已更改。
问题的另一部分实际上是使用常量始终。如果你没有以预期的方式在必要的地方使用它,那么你不依赖于常量,而是依赖于计算(或magic numbers)。
答案 7 :(得分:0)
其中一些示例是配置项而不是常量。在这种情况下-即使您在短期内使用常量,也应将其注入到使用它们的位置,并对该代码进行正常测试。如果无法在运行时更改该值,则您可以也使用围绕该值的示例测试,如下所述。
如果您要测试的东西确实是一个常数(例如const FEET_PER_METRE = 3.28084
),则只需一个示例测试即可将计算和值都包装在一起(该测试的数据应来自独立的来源,而不只是来自根据声明的常量自己计算)。
Assert.AreEqual<decimal>(29.5276, converter.metresToFeet(9))
这种测试有两个目的:
1)如果结果证明您的示例正确,并且常量错误,则可以使用正确的示例更新测试,获得失败的测试,然后将常量修复为绿色。
2)提供回归安全性。如果有人不小心更改了常数(例如,在编辑文件中的其他内容时更改了错误的值或用手指指着该值),那么他们将通过一次失败的测试来警告他们。