我推測,因爲你提到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:TDD的優秀例子。 – Johnsyweb 2011-05-29 05:05:26
感謝您的詳細解答。你給了我很多東西來咀嚼和思考。唯一讓我感到困擾的是你的例子,GameMenu初始化器在添加了很多命令後會變得很長。如果我必須跟蹤我的新的「顯示映射」命令在列表中向下說10個參數,那麼測試它將很容易搞砸。任何這個好的解決方案? – Dty 2011-05-29 12:23:53
@Dty絕對。我曾考慮過這一點。我認爲,對於這個小例子來說,這不是什麼大不了的事情,但你確認它是/可能的。有幾種方法可以處理它。首先想到的是添加一個
register_menu_command
可以被稱爲外部註冊命令。第二個是用_Builder Pattern_替換該參數列表,然後傳入一個生成哈希的MenuBuilder。您可以在測試中配置生成器。我可能會更喜歡builder解決方案。 – bcarlso 2011-05-29 17:45:31