TDD:任何不断测试的模式?

时间:2009-07-06 19:18:54

标签: c# testing tdd constants

常量是美丽的人 - 他们可以在一个独特的地方保存一个在代码中无处不在的值。更改该值只需要一个简单的修改。

生活很酷。

嗯,这是承诺。现实有点不同:

  • 您将LogCompleteFileName常量值从L:\LOGS\MyApp.log更改为\\Traces\App208.txt,您会得到两个文件:跟踪的\\traces\App208.txt和日志的\\traces\App208.txt.log
  • 您将TransactionTimeout从2分钟更改为4分钟,并且在2分钟后仍然会超时(在花了一天之后,您会发现您还必须更改DBMS的超时和事务超时管理器...)。
  • 您将SleepTimeInMinutes1替换为10并且您看不到任何更改(大约一小时后,您会发现该常量的名称具有误导性:粒度不是分钟但毫秒......)。
  • 更加微妙:您将CompanyName更改为Yahoo更改为Microsoft,但自动邮件提醒仍会发送至alert@yahoo.com ...

创建常量是契约。你告诉你的读者,只要他们改变了价值,它仍然会按照他们认为应该的方式运作。

没什么。

当然,您需要测试一下您是否误导了您的读者。你必须确保隐含的合同是正确的。

您如何通过TDD实现这一目标?我只是坚持这一点。我可以测试常量(!)值的变化的唯一方法是使应用程序设置保持不变...当我认为值可以和时,我必须得出结论:const关键字应该被避免会改变吗?

您如何使用TDD测试您的(所谓的)常量?

非常感谢提前:)

8 个答案:

答案 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)

有两种常数:

1)便利性/可读性的常量

使用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());
    }

2)通用常数

同一个应用程序还有一些永远不会改变的常量,例如米和英尺之间的比例。这些值可以/应该硬编码到测试中:

    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)

那是因为所有这些事情都不是常数......事实上是:

  • G(6.67300×10-11 m3 kg-1 s-2)
  • c(299 792 458 m / s)
  • pi(3.1415926535897931)
  • L(6.0221415×1023mol-1)
  • ...

如果这些事情发生变化,请不要担心,您的应用程序崩溃将是最后一个重要的事情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)提供回归安全性。如果有人不小心更改了常数(例如,在编辑文件中的其他内容时更改了错误的值或用手指指着该值),那么他们将通过一次失败的测试来警告他们。