【问题标题】:How to effectively test a fixed length flat file parser using MSpec?如何使用 MSpec 有效地测试固定长度的平面文件解析器?
【发布时间】:2011-10-28 01:32:13
【问题描述】:

我有这个方法签名:List<ITMData> Parse(string[] lines)

ITMData 有 35 个属性。

您将如何有效地测试这样的解析器?

问题:

  • 我应该加载整个文件(我可以使用 System.IO)吗?
  • 我应该将文件中的一行放入字符串常量中吗?
  • 我应该测试一行还是多行
  • 我应该测试 ITMData 的每个属性还是应该测试整个对象?
  • 我的测试命名如何?

编辑

我将方法签名更改为ITMData Parse(string line)

测试代码:

[Subject(typeof(ITMFileParser))]
public class When_parsing_from_index_59_to_79
{
    private const string Line = ".........";
    private static ITMFileParser _parser;
    private static ITMData _data;

    private Establish context = () => { _parser = new ITMFileParser(); };

    private Because of = () => { _data = _parser.Parse(Line); };

    private It should_get_fldName = () => _data.FldName.ShouldBeEqualIgnoringCase("HUMMELDUMM");
}

编辑 2

我仍然不确定是否应该只测试每个类一个属性。在我看来,这允许我为规范提供更多信息,即当我解析从索引 59 到索引 79 的单行时,我得到 fldName。如果我在一个类中测试所有属性,我会丢失这些信息。我是否过度指定了我的测试?

我的测试现在看起来像这样:

[Subject(typeof(ITMFileParser))]
public class When_parsing_single_line_from_ITM_file
{
    const string Line = ""

    static ITMFileParser _parser;
    static ITMData _data;

    Establish context = () => { _parser = new ITMFileParser(); };

    private Because of = () => { _data = _parser.Parse(Line); };

    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    ...

}

【问题讨论】:

  • 你所说的“有效”到底是什么意思?您想尽量减少单元测试的开发时间吗?
  • 好吧,即用一个字符串或多个字符串进行测试?使用仅包含我想要断言的值的字符串进行测试或获取整个字符串。
  • 我仍然不能 100% 确定问题到底是什么,但我会创建一组不同的单元测试。一种测试方法可能会传入一个特定的单个字符串,并断言它已被正确解析为生成的 IMTData 对象的相关属性。也许是另一个测试给定 20 行的测试,它会生成一个包含 20 个项目的列表。对于特定的困难情况,我可能有其他方法。这几乎就是@Kenny 所描述的基本单元测试内容。获取此 Parse 方法的要求,并将其逆向工程为几个证明正确性的测试用例。
  • 我不知道你想如何测试它,但是方法签名是一个错误。接受一个字符串数组进行解析是没有意义的;这鼓励人们将整个文件读入内存并将其放入数组中,这几乎可以肯定是一件愚蠢的事情。接受TextReaderIEnumerable<string> 可能更合适。 (返回一个列表也很可疑,如果你能侥幸地逐行进行解析的话。)
  • 将整个文件读入内存并不傻。这取决于文件大小。我必须处理大约 20 MB 大的文件。 Ofc 大文件不应加载到内存中。

标签: c# unit-testing parsing flat-file mspec


【解决方案1】:

我应该加载整个文件(我可以使用 System.IO)吗?

如果你这样做,它就不再是一个单元测试——它变成了一个集成或回归测试。如果您希望它显示单元测试不会显示的可能错误,您可以这样做。但这不太可能。

单元测试可能会更好,至少在开始时。

我应该将文件中的一行放入字符串常量中吗?

如果您打算编写多个使用相同输入行的测试,那么当然可以。但就个人而言,我可能倾向于编写一堆不同的测试,每个测试都传递不同的输入字符串。在这一点上,没有太多理由创建一个常量(除非它是一个局部常量,在测试方法中声明)。

我应该测试一行还是多行?

您没有指定,但我将假设您的输出与输入是一对一的——也就是说,如果您传入三个字符串,您将返回三个 ITMDatas .在这种情况下,对多行测试的需求将受到限制。

几乎总是值得测试退化的情况,在这种情况下,它是一个空字符串数组(零行)。而且可能值得至少进行一次包含多行的测试,这样您就可以确保迭代中没有愚蠢的错误。

但是,如果您的输出与输入是一对一的,那么您确实有另一种方法想要退出——您应该有一个ParseSingleLine 方法。那么你的Parse 只不过是迭代行并调用ParseSingleLine。您仍然需要对 Parse 进行少量测试,但您的大部分测试将集中在 ParseSingleLine 上。

【讨论】:

    【解决方案2】:

    如果我遇到这样的问题,我通常会这样做:

    提前一个简短的免责声明:我认为我会更多地走“集成测试”或“作为一个整体测试解析器”的路线,而不是测试单个行。在过去,我不止一次遇到过很多实现细节泄漏到我的测试中的情况,当我更改实现细节时,我不得不经常更改测试。我猜是过度规范的典型案例;-/

    1. 我不会在解析器中包含文件加载。正如@mquander 建议的那样,我宁愿使用 TextReader 或 IEnumerable 作为输入参数。这将导致更快的测试,因为您可以在内存中指定解析器输入并且不必接触文件系统。
    2. 我不是手动滚动测试数据的忠实拥护者,因此在大多数情况下,我使用嵌入式资源和 ResourceManager 通过 assembly.GetManifestResource() 直接从规范程序集中加载测试数据。我的解决方案中通常有一堆扩展方法来简化资源的读取(例如 TextReader TextResource.Load("NAME_OF_SOME_RESOURCE"))。
    3. 关于 MSpec:我对每个文件使用一个类进行解析。对于在解析结果中测试的每个属性,我都有一个单独的 (It) 断言。这些通常是一个衬里,因此额外的编码量并不大。在文档和诊断方面,恕我直言,这是一个巨大的优势,因为当没有正确解析属性时,您可以直接看到哪个断言失败,而无需查看源代码或搜索行号。它也出现在您的 MSpec 结果文件中。此外,您不会隐藏其他失败的断言(您修复一个断言只是为了看到规范在下一行与下一个断言失败的情况)。这当然会迫使您更多地考虑您在规范中使用的措辞,但对我来说,这也是一个巨大的优势,因为我支持语言形成思维的想法。换句话说,如果您不知道如何为您的断言命名,那么您的规范或实现可能有些可疑。
    4. 关于解析器的方法签名:我不会返回像 List 或数组这样的具体类型,我还建议不要返回可变 List 类型。你在这里基本上说的是:“嘿,我完成后你可以乱搞解析结果”,这在大多数情况下可能是你不想要的。我建议改为返回 IEnumerable (或 ICollection 如果您以后真的需要修改它)

    【讨论】:

    • 到目前为止谢谢 :) 1. 我解析一行,因为每一行都以相同的方式解析 2. 我会关心将我的测试数据放入资源中。 3.我不能每个文件类型使用一个类,因为有一些特殊的逻辑取决于配置。所以有些属性必须单独测试。 4. 我选择只解析一行。
    【解决方案3】:

    我通常会尝试考虑常见的成功和失败场景以及边缘情况。需求也有助于设置适当的用例。考虑Pex 列举各种场景。

    【讨论】:

      【解决方案4】:

      关于您的新问题:

      我应该测试 ITMData 的每个属性还是应该测试整个对象?

      如果您想安全起见,您可能应该至少有一个测试来检查每个属性是否匹配。

      我的测试命名如何?

      关于这个话题的讨论也不少,比如this one。一般规则是您的单元测试类中有多个方法,每个方法都旨在测试特定的东西。在你的情况下,它可能是这样的:

      public void Check_All_Properties_Parsed_Correctly(){.....}
      
      public void Exception_Thrown_If_Lines_Is_Null(){.....}
      
      public void Exception_Thrown_If_Lines_Is_Wrong_Length(){.....}
      

      因此,换句话说,测试您认为解析器“正确”的确切行为。完成此操作后,您在更改解析器代码时会感到更加轻松,因为您将拥有一个全面的测试套件来检查您没有破坏任何东西。记住要经常进行实际测试,并在进行更改时保持测试更新! MSDN 上有一个关于单元测试和测试驱动开发的相当好的指南。

      总的来说,我认为您可以通过谷歌搜索找到大多数问题的答案。还有几本关于测试驱动开发的优秀书籍,它们不仅会带您了解 TDD 的如何,还可以带您了解为什么。如果您相对编程语言不可知,我会推荐 Kent Beck 的 Test Driven Development By Example,否则类似 Test-Driven Development in Microsoft .NET。这些应该会让你很快走上正轨。

      编辑:

      我是否过度指定了我的测试?

      在我看来,是的。具体来说,我不同意你的下一行:

      如果我测试一个类中的所有属性,我会丢失此信息。

      您究竟是通过什么方式丢失信息的?假设有 2 种方法可以进行此测试,除了每个测试都有一个新类:

      1. 每个属性都有不同的方法。您的测试方法可以称为CheckPropertyXCheckPropertyY 等。当您运行测试时,您将确切地看到哪些字段通过了,哪些字段失败了。这显然满足了您的要求,尽管我会说这仍然是矫枉过正。我会选择选项 2:
      2. 有几种不同的方法,每种方法都测试一个特定的方面。这是我最初推荐的,我想你指的是什么。当其中一个测试失败时,您只会获得有关每个方法失败的第一件事的信息,但如果您对 Assert 进行了很好的编码,您将准确地知道哪个属性不正确。考虑以下代码:

      Assert.AreEqual("test1", myObject.PropertyX, "Property X was incorrectly parsed"); Assert.AreEqual("test2", myObject.PropertyY, "Property Y was incorrectly parsed");

      当其中一个失败时,您将知道哪一行失败。修复相关错误并重新运行测试后,您将查看是否有任何其他属性失败。这通常是大多数人采用的方法,因为为每个属性创建一个类甚至方法会导致代码过多,并且需要进行太多工作来保持最新状态。

      【讨论】:

      • 好的,我回复了您的编辑,我的建议是最多为每个属性创建一个新方法来测试该属性。
      • "你究竟以什么方式丢失信息?"Machine.Specification 允许生成 HTML 报告。如您所见,我的测试以“When_...”开头,因此类名是规范的一部分。如果我为每个属性编写一个类,我将有一个特定的规范,其中包含用于获取特定属性的索引。
      猜你喜欢
      • 2017-11-30
      • 1970-01-01
      • 2011-01-25
      • 1970-01-01
      • 1970-01-01
      • 2020-08-19
      • 1970-01-01
      • 2012-05-17
      • 1970-01-01
      相关资源
      最近更新 更多