C# 消除测试驱动开发中的重复

C# 消除测试驱动开发中的重复,c#,java,unit-testing,testing,tdd,C#,Java,Unit Testing,Testing,Tdd,我正在开发一个小型解析器类TDD样式。以下是我的测试: ... [TestMethod] public void Can_parse_a_float() { InitializeScanner("float a"); Token expectedToken = new Token("float", "a"); Assert.AreEqual(expectedToken, scanner.NextToken());

我正在开发一个小型解析器类TDD样式。以下是我的测试:

    ...

    [TestMethod]
    public void Can_parse_a_float() {
        InitializeScanner("float a");
        Token expectedToken = new Token("float", "a");

        Assert.AreEqual(expectedToken, scanner.NextToken());
    }

    [TestMethod]
    public void Can_parse_an_int() {
        InitializeScanner("int a");
        Token expectedToken = new Token("int", "a");

        Assert.AreEqual(expectedToken, scanner.NextToken());            
    }

    [TestMethod]
    public void Can_parse_multiple_tokens() {
        InitializeScanner("int a float b");

        Token firstExpectedToken = new Token("int", "a");
        Token secondExpectedToken = new Token("float", "b");

        Assert.AreEqual(firstExpectedToken, scanner.NextToken());
        Assert.AreEqual(secondExpectedToken, scanner.NextToken());
    }
困扰我的是,上一个测试使用的代码行与
可以解析\u a\u float()
可以解析\u an\u int()
的代码行相同。一方面,它执行的是这两种方法都不具备的功能:从源代码字符串中,我可以获得几个令牌。另一方面,如果
可以解析浮点数()
可以解析浮点数()
失败,
可以解析多个令牌()
也会失败

我觉得这里有4个目标岌岌可危:

  • 我希望我的测试能够显示我的
    解析器
    解析int
  • 我想让我的测试显示我的
    解析器
    解析浮动
  • 我希望我的测试显示我的
    解析器可以解析一行中的几个整数/浮点
  • 我希望我的测试也能很好地充当文档机制(!)

我向任何分享他对如何更好地处理这种情况的看法的人提供cookies。谢谢

那么,我的问题是——在编写通过前两个测试的代码时,您是做了最简单的事情,还是在了解第三个需求(测试)的情况下提前工作?如果您编写了尽可能最简单的代码,并且它通过了第三次测试而没有编写新代码,那么该测试就没有必要了。如果您必须修改代码,那么需要第三个测试来定义代码。是的,它们现在都在执行相同的代码行,但是这些代码行(应该)不同,因为您已经编写了第三个测试。我看这没有问题。

在我看来,您的担心表明存在设计问题。您有两个独立的职责:

  • 将输入字符串的前缀转换为标记
  • 重复上述操作,跟踪输入字符串中的“我在哪里”
您当前的设计将这两个对象分配给一个解析器对象。这就是为什么不能分别测试这两个职责。更好的设计是为第一个职责定义一个标记器,为第二个职责定义一个解析器。对不起,我对C语言太生疏了,所以我将编写Java

标记器中的一个设计问题是它应该返回两个值:标记和字符串的消耗量。在Java中,字符串是不可变的,所以我不能更改输入参数。我会使用StringReader,这是一个可以使用的字符流。我的第一个测试可能是:

@Test public void tokenizes_an_integer() {
    Tokenizer tokenizer = new Tokenizer();
    StringReader input = new StringReader("int a rest of the input");

    Token token = tokenizer.tokenize(input);

    assertEquals(new Token("a", "int"), token);
    assertEquals(" rest of the input", input.toString());
}
当这通过时,我可以为第二个职责编写一个测试:

@Test public void calls_tokenizer_repeatedly_consuming_the_input() {
    StringReader input = new StringReader("int a int b");
    Parser parser = new Parser(input, new Tokenizer());

    assertEquals(new Token("a", "int"), parser.nextToken());
    assertEquals(new Token("b", "int"), parser.nextToken());
    assertEquals(null, parser.nextToken());
}
这是更好的,但从测试可维护性的角度来看,仍然不是完美的。如果您决定更改“int”标记的语法,两个测试都将中断。理想情况下,你只希望第一个打破。一个解决方案是在第二个测试中使用假标记器,这不依赖于真实的标记器

这是我仍在努力掌握的东西。一个有用的资源是《成长中的面向对象软件》一书,这本书在测试独立性和表达能力方面非常好


饼干在哪里?:-)

这种情况偶尔也发生在我身上。如果你坚持失败-通过-重构周期,有时你会发现两个测试在代码和逻辑方面实际上变成了同一个测试

对这种情况的适当响应各不相同,但如果您将测试视为技术规范而不是测试,则会变得更加清晰。如果测试传达了关于被测试类应该做什么的独特信息,那么就保留它。如果是重复的规范,请将其删除。我经常保持这样的测试,后来对行为进行了更改,使它们再次从代码路径的角度变得独特。如果我决定放弃该规范,我可能会错过以后的新代码路径


您的第三次测试提供的vale的真正单元是描述并坚持正确的多令牌行为。对我来说,这似乎是这组测试中的一个独特规范,所以我会保留它。

每一个测试都为类逻辑添加了一些内容。也就是说,它在一开始就是一个失败的测试。但是我在这里要问的不是设计阶段,而是下一个阶段,在下一个阶段,我更担心代码复制和测试可维护性,而不是如何驱动我的类的设计。@Ownede-那么要问的问题是“我是否可以取消此测试并确保不会破坏某些内容,“即,测试是否不再与代码相关?我不会考虑它是否只是行使相同的行,因为你没有添加它来覆盖更多的代码行,你添加它来驱动一个特性。只有在不需要特性时才删除它。第三个测试用例不应该确保您的标记化工作正常吗?可能有一段未经测试的代码在解析两个标记(已经测试过)之前分割输入。让我思考的不是我是否应该进行第三次测试——而是我是否应该进行前两次测试。答案相同——如果这些测试描述的是第三次测试以外的单独规范。在我看来,他们可能会这样做,但这只能在你的领域背景下决定。一个可能的启发是——如果有人只阅读你测试的标题,仍然对规范有一个很好的了解,那么你肯定应该摆脱他们