2011-05-28 141 views
5

我需要一些TDD概念的幫助。說我有下面的代碼調用其他方法的TDD方法的正確方法

def execute(command) 
    case command 
    when "c" 
    create_new_character 
    when "i" 
    display_inventory 
    end 
end 

def create_new_character 
    # do stuff to create new character 
end 

def display_inventory 
    # do stuff to display inventory 
end 

現在我不知道該寫什麼我的單元測試。如果我爲execute方法編寫單元測試,那麼這不會涵蓋我對create_new_characterdisplay_inventory的測試嗎?或者我在那個時候測試了錯誤的東西?我的execute方法測試只測試執行是否傳遞給正確的方法並在那裏停止?那麼我應該寫更多的單元測試,專門測試create_new_characterdisplay_inventory

回答

6

我推測,因爲你提到TDD有問題的代碼實際上並不存在。如果確實如此,那麼你不是在做真正的TDD,而是TAD(測試後開發),這自然會導致這樣的問題。在TDD中,我們從測試開始。看來你正在構建某種類型的菜單或命令系統,所以我將以此爲例。

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    # Assuming character creation would require capturing additional 
    # information it violates SRP (Single Responsibility Principle) 
    # and belongs in a separate class so we'll mock it out. 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 

    # Using constructor injection to tell the code about the mock 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 
end 

這個測試會導致一些代碼類似於以下(請記住,足夠的代碼以使測試通過,沒有更多)

class GameMenu 
    def initialize(character_creation_command) 
    @character_creation_command = character_creation_command 
    end 

    def execute(command) 
    @character_creation_command.execute 
    end 
end 

現在,我們將添加一個測試。

it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
end 

運行這個測試將帶領我們實現如:

class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @inventory_command = inventory_command 
    end 

    def execute(command) 
    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 
end 

此實現使我們對我們的代碼的問題。我們的代碼在輸入無效命令時應該做什麼?一旦我們決定了這個問題的答案,我們就可以實施另一項測試。

it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
end 

驅動出一個快速變化的execute方法

def execute(command) 
    unless ["c", "i"].include? command 
     raise ArgumentError("Invalid command '#{command}'") 
    end 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

現在,我們已經通過測試我們可以使用提取方法重構到命令的驗證提取到一個意向揭示方法

def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 

    if command == "i" 
     @inventory_command.execute 
    else 
     @character_creation_command.execute 
    end 
    end 

    def invalid?(command) 
    !["c", "i"].include? command 
    end 

現在我們終於明白了,我們可以解決您的問題。由於invalid?方法是通過重構受測試的現有代碼而被驅除的,因此不需要爲它編寫單元測試,它已經被覆蓋並且不會獨立運行。由於庫存和字符命令沒有通過我們現有的測試進行測試,因此需要獨立進行測試驅動。

請注意,當測試通過時,我們的代碼仍然可以更好,讓我們把它清理一下。條件語句表示我們違反了ODP(開放 - 封閉原則)我們可以使用替代條件與多態重構刪除條件邏輯。

# Refactored to comply to the OCP. 
class GameMenu 
    def initialize(character_creation_command, inventory_command) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

現在我們已經重構了類,使得額外的命令只需要我們的命令哈希,而不是改變我們的條件邏輯還有invalid?方法添加額外的條目。

所有的測試仍然應該通過,我們幾乎完成了我們的工作。一旦我們試駕的各個命令,你可以回到初始化方法,並添加一些默認值,像這樣的命令:

def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

最後的測試是:

describe GameMenu do 
    it "Allows you to navigate to character creation" do 
    character_creation = mock("character creation") 
    character_creation.should_receive(:execute) 
    menu = GameMenu.new(character_creation) 
    menu.execute("c") 
    end 

    it "Allows you to display character inventory" do 
    inventory_command = mock("inventory") 
    inventory_command.should_receive(:execute) 
    menu = GameMenu.new(nil, inventory_command) 
    menu.execute("i") 
    end 

    it "Raises an error when an invalid command is entered" do 
    menu = GameMenu.new(nil, nil) 
    lambda { menu.execute("invalid command") }.should raise_error(ArgumentError) 
    end 
end 

,最終GameMenu樣子:

class GameMenu 
    def initialize(character_creation_command = CharacterCreation.new, 
       inventory_command = Inventory.new) 
    @commands = { 
     "c" => character_creation_command, 
     "i" => inventory_command 
    } 
    end 

    def execute(command) 
    raise ArgumentError("Invalid command '#{command}'") if invalid? command 
    @commands[command].execute 
    end 

    def invalid?(command) 
    [email protected]_key? command 
    end 
end 

希望有所幫助!

布蘭登

+1

+1:TDD的優秀例子。 – Johnsyweb 2011-05-29 05:05:26

+0

感謝您的詳細解答。你給了我很多東西來咀嚼和思考。唯一讓我感到困擾的是你的例子,GameMenu初始化器在添加了很多命令後會變得很長。如果我必須跟蹤我的新的「顯示映射」命令在列表中向下說10個參數,那麼測試它將很容易搞砸。任何這個好的解決方案? – Dty 2011-05-29 12:23:53

+0

@Dty絕對。我曾考慮過這一點。我認爲,對於這個小例子來說,這不是什麼大不了的事情,但你確認它是/可能的。有幾種方法可以處理它。首先想到的是添加一個register_menu_command可以被稱爲外部註冊命令。第二個是用_Builder Pattern_替換該參數列表,然後傳入一個生成哈希的MenuBuilder。您可以在測試中配置生成器。我可能會更喜歡builder解決方案。 – bcarlso 2011-05-29 17:45:31

3

考慮重構,使得具有(在你的情況execute)解析命令責任的代碼是獨立實現的操作代碼(即create_new_characterdisplay_inventory)。這樣可以很容易地將操作模擬出來並獨立地測試命令解析。 想要獨立測試不同的作品。

+0

我不確定我明白你的意思。例如,我已經感覺到命令的解析和動作的執行是獨立的。你能告訴我一個你的意思嗎?也許這會幫助我理解。 – Dty 2011-05-28 10:17:17

0

我會給create_new_characterdisplay_inventory正常的試驗,終於測試execute,僅僅是一個包裝函數,設定期望檢查apropriate命令被稱爲(並返回結果)。類似的東西:

def test_execute 
    commands = { 
    "c" => :create_new_character, 
    "i" => :display_inventory, 
    } 
    commands.each do |string, method| 
    instance.expects(method).with().returns(:mock_return) 
    assert_equal :mock_return, instance.execute(string) 
    end 
end