2011-03-05 12 views
2

我正在開發一個小的解析器類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解析整數
  • 我想我的測試表明我的Parser解析花車
  • 我想我的測試表明,我的Parser可以解析幾個整數/浮點連續的數字
  • 我想我的測試也可以很好地作爲文檔機制(!)

我向任何人分享他對如何更好地處理這種情況的意見的任何人提供餅乾。謝謝!

+0

第三個測試用例不應該確保您的標記化工作嗎?在解析兩個標記(已經過測試)之前,可能會有一段未經測試的代碼將輸入拆分。 – 2011-03-05 19:02:21

回答

4

所以,我的問題是 - 當你編寫通過前兩個測試的代碼,或者你知道第三個需求(測試)時,你是否做過最簡單的事情?如果您編寫了最簡單的代碼,並且在不編寫新代碼的情況下通過了第三次測試,那麼測試就沒有必要了。如果您必須修改代碼,那麼需要進行第三次測試並用於定義代碼。是的,他們現在都是三個行使相同的代碼行,但是現在你已經寫了第三個測試,這些行是(應該)不同。我沒有看到這個問題。

+0

每一個測試都給類邏輯添加了一些東西。也就是說,它的成立是一個失敗的考驗。 – 2011-03-05 18:58:22

+0

然而,我在這裏提出的問題與設計階段無關,而是與下一個階段有關,我更擔心代碼複製和測試可維護性,而不是如何推動我的課程設計。 – 2011-03-05 19:00:21

+1

@devoured - 那麼要問的問題是「我可以刪除這個測試並確保我不會破壞某些東西」,即測試不再與代碼相關嗎?我不會考慮它是否僅執行相同的行,因爲您沒有添加它來覆蓋更多的代碼行,而是添加它來驅動一個功能。只有在不需要該功能時才能將其刪除。 – tvanfosson 2011-03-05 19:03:47

2

在我看來,您的關注表明存在設計問題。你有兩個獨立的責任:

  • 將輸入字符串的前綴令牌
  • 重複上面的操作跟蹤的「我在哪裏」,在輸入字符串

當前的設計將這兩個分配給一個解析器對象。這就是爲什麼你不能單獨測試兩個責任。更好的設計是爲第一責任定義一個Tokenizer,然後爲第二個責任定義一個Parser。對不起,我在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」標記的語法,則兩個測試都會中斷。理想情況下,你只想第一個打破。一種解決方案是在第二個測試中使用僞造的tokenizer,它不依賴於真實的tokenizer。

這是我仍然試圖掌握的東西。一個有用的資源是「增長的面向對象軟件」一書,它在測試獨立性和表達性方面非常出色。

餅乾在哪裏? :-)

1

這偶然也發生在我身上。如果你堅持fail-pass-refactor循環,有時候你會發現兩個測試實際上是按照他們所運行的代碼和邏輯進行的。

對這種情況的適當反應各不相同,但如果將測試視爲技術規範而不是測試,則會變得更加清晰。如果測試傳達了一些獨特的東西,那麼測試類應該做什麼,而不是保留它。如果它是重複的規格,請刪除它。我經常保持這樣的測試,並且稍後對行爲進行更改,使得它們再次從代碼路徑的角度變得獨一無二。如果我決定放棄這個規範,我可能會在稍後錯過新的代碼路徑。

您的第三次測試提供的vale的實際單位是描述並堅持正確的多標記行爲。這對我來說似乎是一套獨特的測試規範,所以我會保留它。

+0

是什麼讓我想到的不是我是否應該拿出第三個測試 - 是我應該拿出前兩個出局還是不出場。 – 2011-03-06 15:37:45

+0

相同的答案 - 如果這些測試描述了一個單獨的規格比第三個。在我看來,他們可能會這樣做,但它只能真正在您的域名背景下決定。一個可能的啓發是這樣的 - 如果有人只讀了你的測試的標題仍然對規範有一個好主意,那麼你肯定應該擺脫它們 – tallseth 2011-03-06 15:49:49