如何使用MSpec有效地测试固定长度的平面文件解析器

本文关键字:平面文件 MSpec 何使用 有效地 测试 | 更新日期: 2023-09-27 18:05:39

我有这个方法签名: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(???);
    ...
}

如何使用MSpec有效地测试固定长度的平面文件解析器

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

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

你最好使用单元测试,至少在开始的时候。

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

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

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

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

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

然而,如果您的输出与您的输入是一对一的,那么您真的有另一个想要输出的方法—您应该有一个ParseSingleLine方法。那么您的Parse将仅仅是迭代行并调用ParseSingleLine。您仍然需要对Parse进行一些测试,但是您的大部分测试将集中在ParseSingleLine上。

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

提前一个简短的免责声明:我认为我更倾向于采用"集成测试"或"将解析器作为一个整体进行测试"的路线,而不是测试单个行。在过去,我不止一次面临这样的情况:大量的实现细节泄露到我的测试中,当我更改实现细节时,我不得不经常更改测试。我猜这是典型的规格过高;-/

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

我通常会考虑常见的成功和失败场景,以及边缘情况。需求对于建立适当的用例也很有帮助。考虑使用Pex来枚举各种场景。

关于你的新问题:

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

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

我的测试的命名呢?

关于这个话题有很多讨论,比如这个。一般规则是在单元测试类中有多个方法,每个方法都旨在测试特定的东西。在您的例子中,它可能是这样的:
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》,或者类似于Microsoft . net中的测试驱动开发。这些应该会让你很快走上正轨。 编辑:

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

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

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

你到底是怎么丢失信息的?假设有两种方法来做这个测试,除了每个测试有一个新类:

  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");

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