2011-11-20 52 views
4

我經常發現,在編寫測試方法時,我想在該方法中拋出一堆不同的輸入,並簡單地檢查輸出是否符合我的預期。作爲一個微不足道的例子,假設我正在測試my_square_function,它正方形數字並智能地處理nil用於測試多個數據點的rspec設計模式

下面的代碼似乎做的工作,但我不知道是否有,我應該使用(例如使用subjectcontext)最佳實踐:

describe "my_square_function" do 
    @tests = [{:input => 1, :result => 1}, 
      {:input => -1, :result => 1}, 
      {:input => 2, :result => 4}, 
      {:input => nil, :result => nil}] 
    @tests.each do |test| 
    it "squares #{test[:input].inspect} and gets #{test[:result].inspect}" do 
     my_square_function(test[:input]).should == test[:result] 
    end 
    end 
end 

建議?

謝謝!

(相關:rspec refactoring?

回答

5

對不起,這麼長回答,但是我認爲如果我全部經歷了這個過程,我的思維過程會更加連貫。

由於此問題標記爲TDD,因此我假定您正在編寫方法TDD樣式。如果是的話,你可能要開始:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 
end 

有一個失敗的測試,你可以實現my_square_function如下:

def my_square_function(number) 
    1 
end 

現在,測試合格要重構出複製。在這種情況下,代碼和測試之間的重複,即文字1.由於參數帶有測試的值,所以我們可以使用參數來刪除重複。

def my_square_function(number) 
    number 
end 

現在,複製已被刪除,測試仍然可以通過,我們可以移動到下一個測試:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 
end 

運行你再次失敗的測試迎來了測試,所以我們使它傳:

def my_square_function(number) 
    number.abs # of course I probably wouldn't really do this but 
       # hey, it's an example. :-) 
end 

現在這個測試通過,它的時間移動到另一個測試:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 
end 

在這一點上,你最新的測試將不再通過,所以現在要讓它通過:

def my_square_function(number) 
    number.abs * number 
end 

哎呀。這不起作用,它導致我們的負數測試失敗。幸運的是,失敗表明我們回到了無效的確切測試,但我們知道由於「負面」測試而失敗。回到代碼:

def my_square_function(number) 
    number.abs * number.abs 
end 

這樣比較好,我們所有的測試都通過了。現在是時候重新構造了。在這裏,我們在abs的調用中看到一些其他不必要的代碼。我們可以擺脫他們:

def my_square_function(number) 
    number * number 
end 

測試仍然通過,我們看到一些與討厭的論點更多的重複。讓我們看看我們是否可以擺脫它:

def my_square_function(number) 
    number ** 2 
end 

測試通過,我們不再有這種重複。現在,我們有一個乾淨的實現,讓我們處理nil情況下一:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

好了,我們又回到了再次失敗,我們可以繼續前進,實現nil檢查:

def my_square_function(number) 
    number ** 2 unless number == nil 
end 

這測試通過,它很乾淨,所以我們會保持原樣。現在我們回到規範,看看我們有什麼,並驗證我們喜歡我們所看到的:

describe "my_square_function" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

我的第一個傾向是,我們真的描述「平方」的數字,行爲不函數本身,所以我們將改變它:

describe "How to square a number" do 
    it "Squares a positive number" do 
    my_square_function(1).should == 1 
    end 

    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

現在,這三個示例名稱在放入該上下文中時會有點鬆動。我將從第一個例子開始,對於方塊1,這似乎有點俗氣。這是我將要減少代碼中的示例數量的選擇。我真的希望這些例子以某種方式變得有趣,否則我不會測試它們。平方1和2之間的區別是無趣的,所以我將刪除第一個例子。它起初是有用的,但不再是。這使得我們有:

describe "How to square a number" do 
    it "Squares a negative number" do 
    my_square_function(-1).should == 1 
    end 

    it "Squares other positive numbers" do 
    my_square_function(2).should == 4 
    end 

    it "Doesn't try to process 'nil' arguments" do 
    my_square_function(nil).should == nil 
    end 
end 

我要看看接下來的事情就是反面的例子,因爲它涉及到在描述塊的上下文。我要去給它和實例的新描述的其餘部分:

describe "How to square a number" do 
    it "Squaring a number is simply the number multiplied by itself" do 
    my_square_function(2).should == 4 
    end 

    it "The square of a negative number is positive" do 
    my_square_function(-1).should == 1 
    end 

    it "It is not possible to square a 'nil' value" do 
    my_square_function(nil).should == nil 
    end 
end 

現在,我們已經限制了測試用例的數量最有趣的,我們真的沒有太多要處理用。正如我們上面看到的那樣,如果發現另一個測試案例,我們不希望發生故障,那麼知道故障發生在哪條線路上真是令人高興。通過構建一個場景列表,我們會失去該功能,從而難以調試失敗。現在,我們可以用另一個解決方案中提到的動態生成的it塊代替示例,但是我們開始失去了我們試圖描述的行爲。因此,總而言之,通過將您的測試場景限制爲僅描述系統有趣特性的場景,您將減少太多場景的需求。在一個更復雜的系統中,有許多場景可能會強調對象模型可能需要另一個外觀。

希望有幫助!

布蘭登

+1

+1爲給出有意義的名稱來測試輸入輸出組合,幫助您瞭解爲什麼需要組合。容易的事情..但很少見。有沒有辦法自定義應該失敗的消息(無需編寫自定義匹配器)? – Gishu

+0

謝謝布蘭登!這是嚴格的TDD的一個很好的解釋。你的方法明確了爲什麼每個數據點都很重要,而不是僅僅在函數中拋出一堆示例。 – brahn

+0

@Gishu我不知道如何在沒有自定義匹配器的情況下自定義消息,雖然它們很容易編寫,所以我不會害怕這樣做。 – bcarlso

10

我會輸入,並在所示更簡單的方式哈希預期的結果聯繫起來,並重復散列:

describe "my_square_function" do 
    @tests = {1 => 1, 
      -1 => 1, 
      2 => 4, 
      nil => nil} 

    @tests.each do |input, expected| 
    it "squares #{input} and gets #{expected}" do 
     my_square_function(input).should == expected 
    end 
    end 
end 
+2

當然好,緊湊! – brahn

+0

在函數接受兩個以上參數的情況下,此方法證明自己更有用。 – pisaruk