单元测试的预期结果应该硬编码吗?


29

单元测试的预期结果应该硬编码还是可以依赖于初始化变量?硬编码或计算结果是否增加了在单元测试中引入错误的风险?我还没有考虑其他因素吗?

例如,这两种格式中哪一种是更可靠的格式?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

编辑1:针对DXM的回答,选项3是首选解决方案吗?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

2
两者都做。说真的 测试可以并且应该重叠。如果您发现自己正在处理硬编码的值,还请研究某种数据驱动的测试。
Job

我同意第三个选择是我想要使用的。我不认为选项1会受到伤害,因为您消除了对编译的操纵。
kwelch 2012年

您的两个选项都使用硬编码,但是如果测试未在C:\\上运行,则会中断
Qwertie

Answers:


27

我认为计算出的期望值会导致更健壮和灵活的测试用例。同样,通过在表达式中使用良好的变量名来计算预期结果,可以更清楚地知道预期结果最初来自何处。

话虽如此,在您的特定示例中,我不信任“ Softcoded”方法,因为它使用您的SUT(被测系统)作为计算的输入。如果MyClass中存在一个错误,其中字段未正确存储,则您的测试实际上将通过,因为您的期望值计算将使用与target.GetPath()相同的错误字符串。

我的建议是在合理的地方计算期望值,但要确保计算不依赖于SUT本身的任何代码。

回应OP对我的回应的更新:

是的,根据我的知识,但在进行TDD方面的经验有限,我会选择选项#3。


1
好点子!不要依赖测试中未经验证的对象。
Hand-E-Food

它不是SUT代码的重复吗?
Abyx 2011年

1
从某种意义上讲,但这就是您验证SUT是否正常工作的方式。如果我们使用相同的代码而被淘汰,您将永远不会知道。当然,如果要执行计算,您需要重复很多SUT,那么选项#1可能会变得更好,只需对值进行硬编码即可。
DXM

16

如果代码如下:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

您的第二个示例无法捕获该错误,但是第一个示例可以捕获该错误。

通常,我建议不要使用软编码,因为它可能会隐藏错误。例如:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

你能发现问题吗?在硬编码版本中,您不会犯同样的错误。与硬编码的值相比,更难获得正确的计算结果。这就是为什么我更喜欢使用硬编码值而不是软编码值。

但是也有例外。如果您的代码必须在Windows和Linux上运行怎么办?不仅路径必须不同,而且还必须使用不同的路径分隔符!在这种情况下,使用抽象两者之间差异的函数来计算路径可能是有意义的。


我听到你在说什么,这使我有一点考虑的余地。软编码依赖于我通过的其他测试用例(例如ConstructorShouldCorrectlyInitialiseFields)。您描述的故障将由其他失败的单元测试交叉引用。
Hand-E-Food

@ Hand-E-Food,听起来您正在针对对象的各个方法编写测试。别。您应该编写测试来检查整个对象(而不是单个方法)的正确性。否则,测试会因对象内部的变化而变脆。
温斯顿·埃韦特

我不确定是否要遵循。我给出的示例纯粹是假设的,很容易理解。我正在编写单元测试来测试类和对象的公共成员。那是使用它们的正确方法吗?
Hand-E-Food

@ Hand-E-Food,如果我对您的理解正确,则您的测试ConstructShouldCorrectlyInitialiseFields将调用构造函数,然后断言字段设置正确。但是你不应该那样做。您不必在意内部字段在做什么。您只能断言该对象的外部行为是正确的。否则,可能是需要替换内部实现的日子。如果您对内部状态做出断言,则所有测试都将失败。但是,如果您仅对外部行为进行断言,那么一切仍然会起作用。
温斯顿·埃韦特

@Winston-我实际上正在研究xUnit Test Patterns一书,在此之前完成了单元测试的艺术。我不会假装我知道我在说什么,但我想我已经从那些书中拿了一些东西。这两本书都强烈建议每种测试方法都应测试绝对最小值,并且您应具有许多测试用例来测试整个对象。这样,当界面或功能发生更改时,您应该只期望修复少数测试方法,而不是大多数。由于它们很小,因此更改应该更容易。
DXM

4

我认为您的两个建议都不理想。做到这一点的理想方法是:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

换句话说,测试应仅基于对象的输入和输出,而不是基于对象的内部状态。该对象应视为黑匣子。(我忽略了其他问题,例如使用string.Join代替Path.Combine的不当性,因为这只是一个示例。)


1
并非所有方法都起作用-许多正确地具有改变某些对象或多个对象状态的副作用。具有副作用的方法的单元测试可能需要评估受该方法影响的对象的状态。
马修·弗林

然后将该状态视为该方法的输出。此示例测试的目的是检查GetPath()方法,而不是MyClass的构造函数。阅读@DXM的答案,他为采用黑匣子方法提供了很好的理由。
Mike Nakis 2011年

@MatthewFlynn,那么您应该测试受该状态影响的方法。确切的内部状态是实现细节,而与测试无关。
温斯顿·埃韦特

为了澄清一下,@ MatthewFlynn与所示示例相关,还是其他单元测试需要考虑的其他内容?我看得出来,无所谓的东西像target.Dispose(); Assert.IsTrue(target.IsDisposed);(一个很简单的例子。)
手-E-食品

即使在这种情况下,IsDisposed属性也是(或应该是)该类的公共接口中必不可少的部分,而不是实现细节。(IDispose接口没有提供这样的属性,但这很不幸。)
Mike Nakis 2011年

2

讨论中有两个方面:

1.使用目标本身作为测试用例
第一个问题是/您是否应该使用类本身来依赖并获得测试存根中完成的工作的一部分?-答案是否定的,因为通常来说,您永远不要对要测试的代码进行假设。如果做得不好,随着时间的流逝,漏洞将无法进行某些单元测试。

2. 硬编码
您应该硬编码吗?同样,答案是否定的。因为像任何软件一样,随着事情的发展,对信息的硬编码变得很困难。例如,当您想要再次修改以上路径时,您需要编写其他单元或继续进行修改。更好的方法是保持输入和评估日期来自易于配置的单独配置。

例如,这就是我如何纠正测试存根。

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

0

可能有很多概念,并举了一些例子来了解它们之间的区别

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

总结一下:一般来说,您的第一个硬编码测试对我来说最有意义,因为它很简单,直截了当等等。如果您开始硬编码路径太多次,只需将其放入设置方法中即可。

为了将来进行更多的结构化测试,我将检查数据源,以便在需要更多测试情况时仅添加更多数据行。


0

现代的测试框架允许您为方法提供参数。我会利用那些:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

我认为,这样做有几个优点:

  1. 开发人员通常很想将看似简单的代码部分从其SUT复制到其单元测试中。正如Winston指出的那样,这些漏洞中仍然可能隐藏着棘手的错误。“硬编码”预期结果有助于避免由于原始代码不正确的原因而导致测试代码不正确的情况。但是,如果需求的变化迫使您跟踪嵌入在数十种测试方法中的硬编码字符串,那可能会很烦人。将所有硬编码值放在测试逻辑之外的某个位置,可以为您带来两全其美的体验。
  2. 您可以使用一行代码为不同的输入和预期的输出添加测试。这鼓励您编写更多测试,同时使测试代码保持DRY并易于维护。我发现,因为添加测试非常便宜,所以我对新的测试用例敞开心mind,如果我不得不为它们编写一种全新的方法,我将不会想到。例如,如果其中一个输入中包含点,我期望什么行为?反斜线?如果一个人空着怎么办?还是空格?还是以空格开头或结尾?
  3. 测试框架会将每个TestCase视为自己的测试,甚至将提供的输入和输出放入测试名称中。如果所有TestCases都通过了测试,那么很容易看到哪一个失败了,以及与其他所有测试有何不同。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.