删除测试驱动开发中的重复

时间:2011-03-05 18:13:54

标签: 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());
    }

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

让我烦恼的是,最后一次测试是使用Can_parse_a_float()Can_parse_an_int()相同的代码行。一方面,它正在运用这些方法都没有的东西:从源代码字符串中,我可以得到几个令牌。另一方面,如果Can_parse_a_float()Can_parse_an_int()失败,Can_parse_multiple_tokens()也会失败。

我觉得这里有4个目标:

  • 我希望我的测试显示我的Parser解析ints
  • 我希望我的测试显示我的Parser解析浮动
  • 我希望我的测试显示我的Parser可以连续解析几个整数/浮点数
  • 我希望我的测试也能充当文档机制(!)

我向任何分享他如何更好地处理这种情况的意见的人提供cookie。谢谢!

3 个答案:

答案 0 :(得分:4)

所以,我的问题是 - 当您编写通过前两个测试的代码或者您是否已经完成工作时,您是否做过最简单的事情,了解第三个要求(测试)?如果您编写了最简单的代码并且它在没有编写新代码的情况下通过了第三次测试,那么该测试就没有必要了。如果必须修改代码,则需要进行第三次测试并用于定义代码。是的,他们现在都是三个行使相同代码的行,但是当你编写第三个测试时,这些行(应该)是不同的。我觉得没问题。

答案 1 :(得分:2)

在我看来,您的担忧表明存在设计问题。你有两个不同的职责:

  • 将输入字符串的前缀转换为标记
  • 重复上述操作,跟踪输入字符串中的“我在哪里”

您当前的设计会将这两个分配给单个Parser对象。这就是为什么你不能分别测试这两个责任的原因。更好的设计是为第一个责任定义一个Tokenizer,为第二个责任定义一个Parser。对不起,我在C#上太生气了所以我会写Java。

tokenizer中的一个设计问题是它应该返回两个值:令牌,以及消耗了多少字符串。在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”标记的语法,则两个测试都将中断。理想情况下,您只希望第一个打破。一种解决方案是在第二次测试中使用伪标记器,这不依赖于真实标记器。

这是我仍在努力掌握的东西。一个有用的资源是“不断增长的面向对象软件”一书,它在测试独立性和表现力方面非常出色。

饼干在哪里? : - )

答案 2 :(得分:1)

偶尔也会发生这种情况。如果您坚持使用fail-pass-refactor循环,有时您会发现两个测试实际上就它们所运用的代码和逻辑而变为同一个。

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

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