2009-11-13 169 views
18

我正在嘗試改進我的Python項目中測試的數量和質量。隨着測試次數的增加,我遇到的一個難題就是了解每個測試的作用以及它應該如何幫助發現問題。我知道跟蹤測試的一部分是更好的單元測試名稱(已解決elsewhere),但我也很想了解文檔和單元測試如何一起進行。如何記錄單元測試?

單元測試在未來測試失敗時如何記錄以提高其效用?具體來說,什麼使得單元測試文檔字符串更好?

我會欣賞這兩個描述性的答案和單元測試的例子與優秀的文檔。雖然我只使用Python,但我願意接受來自其他語言的實踐。

回答

13

我記錄最上我的單元測試有專門的方法名稱:

testInitializeSetsUpChessBoardCorrectly() 
testSuccessfulPromotionAddsCorrectPiece() 

對於我的測試案例幾乎是100%,這顯然解釋了單元測試驗證,這就是我所用。但是,在一些更復雜的測試案例中,我會在整個方法中添加一些註釋來解釋幾行代碼在做什麼。

我以前見過一個工具(我相信它是用於Ruby),它會通過解析項目中所有測試用例的名稱來生成文檔文件,但我不記得名稱。如果你有測試案例的棋女王級:

testCanMoveStraightUpWhenNotBlocked() 
testCanMoveStraightLeftWhenNotBlocked() 

工具將產生與內容是這樣的一個HTML文檔:

Queen requirements: 
- can move straight up when not blocked. 
- can move straight left when not blocked. 
+0

什麼是你的函數名稱?我假設你命名你的測試「testFunctionName」,這是好的,但你真的有一個名爲InitializeSetsUpChessBoardCorrectly函數?我認爲「setUpChessboard」會很好。 – 2009-11-13 02:01:59

+10

不,方法名稱正確地解釋了它正在測試的內容 - 該測試用例驗證了initalize()正確設置了棋盤。繁榮,自動文件。 – 2009-11-13 02:04:35

+0

哈哈是的,開始的「測試」僅僅是JUnit的舊時代,我的大腦仍然堅持在這裏。我可以將它命名爲initalizeSetsUpChessBoardCorrectly()並使用@Test註釋。 – 2009-11-13 02:06:52

4

的測試方法應該正是你是什麼描述名稱測試。文檔應該說明是什麼使測試失敗。

1

您應該在文檔字符串中使用描述性方法名稱和註釋的組合。一個好的方法是在文檔字符串中包含一個基本的過程和驗證步驟。然後,如果您從某種自動運行測試和收集結果的測試框架運行這些測試,則可以讓框架記錄每個測試方法的doc字符串的內容及其stdout + stderr。

這裏有一個基本的例子:

class SimpelTestCase(unittest.TestCase): 
    def testSomething(self): 
     """ Procedure: 
      1. Print something 
      2. Print something else 
      --------- 
      Verification: 
      3. Verify no errors occurred 
     """ 
     print "something" 
     print "something else" 

具有程序與測試使得它更容易找出測試在做什麼。如果您將docstring與測試輸出結合使用,則可以更輕鬆地找出結果後面出現的問題。我曾經工作過的地方做過這樣的事情,發生故障時效果很好。我們使用CruiseControl自動運行每次檢查的單元測試。

+0

http://blog.codinghorror.com/code-tells-you-how-comments-tell-you-why/ – 2014-04-28 16:31:15

8

也許問題不在於如何最好地編寫測試文檔,而是如何編寫測試本身?重構測試的方式使得它們自我記錄可以走得很遠,並且當代碼更改時,文檔字符串不會變陳舊。

有幾件事情可以做,以使測試更清晰:

  • 明確&描述測試方法名稱(已經提到)
  • 檢驗機構應當簡潔明瞭(自我記錄)
  • 抽取複雜的設置/拆卸等方法
  • 更多?

舉例來說,如果你有這樣一個測試:

def test_widget_run_returns_0(): 
    widget = create_basic_widget() 
    return_value = widget.run() 
    assert return_value == 0 
    assert_basic_widget(widget) 

def create_basic_widget(): 
    widget = Widget(param1, param2, "another param") 
    widget.set_option(true) 
    widget.set_temp_dir("/tmp/widget_tmp") 
    widget.destination_ip = "10.10.10.99" 
    return widget 

def assert_basic_widget(): 
    assert widget.response == "My expected response" 
    assert widget.errors == None 

請注意,您的測試方法,現由:

def test_widget_run_returns_0(): 
    widget = Widget(param1, param2, "another param") 
    widget.set_option(true) 
    widget.set_temp_dir("/tmp/widget_tmp") 
    widget.destination_ip = "10.10.10.99" 

    return_value = widget.run() 

    assert return_value == 0 
    assert widget.response == "My expected response" 
    assert widget.errors == None 

你可能有一個方法調用替換的設置語句一系列具有意圖揭示名稱的方法調用,這是一種特定於您的測試的DSL。像這樣的測試是否仍然需要文檔?

另一件需要注意的是,你的測試方法主要是在一個抽象層次上。有人讀取測試方法會看到的算法是:

  • 創建一個widget
  • 呼籲部件
  • 斷言代碼做了什麼,我們預計

他們的測試方法的理解運行不會被設置窗口小部件的細節所迷惑,這是一種比測試方法更低的抽象級別。

該測試方法的第一個版本遵循Inline Setup模式。第二個版本遵循Creation MethodDelegated Setup模式。

通常我反對評論,除非他們解釋代碼的「爲什麼」。閱讀鮑勃馬丁叔叔的Clean Code讓我相信了這一點。有關於評論的章節,並且有關於測試的章節。我推薦它。

有關自動測試最佳做法的更多信息,請檢查xUnit Patterns

+0

謝謝大家了額外的資源,並幫助我理解如何簡化測試本身。我一定會對這個話題做更多的閱讀。再次,謝謝! – ddbeck 2009-11-15 17:07:48

0

當測試失敗時(應該在它通過之前),您應該看到錯誤消息並能夠告訴發生了什麼。只有當你這樣規劃時纔會發生這種情況。

這完全是測試類,測試方法和斷言消息的命名問題。當一個測試失敗時,你無法從這三個線索中判斷出什麼,然後重命名一些東西或者分解一些測試類。

如果fixture的名稱是ClassXTests,並且測試的名稱是TestMethodX,並且錯誤消息是「expected true,returned false」,則不會發生這種情況。這是一個馬虎測試寫作的跡象。

大多數情況下,您不必閱讀測試或任何意見以瞭解發生了什麼。