我需要一些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
Run Code Online (Sandbox Code Playgroud)
现在我不确定要为我的单元测试编写什么.如果我为该execute方法编写单元测试并不能完全覆盖我的测试create_new_character和display_inventory?或者我在那时测试错误的东西?我对该execute方法的测试是否只测试执行是否传递给正确的方法并停在那里?然后,我应该写更多的单元测试是专门测试create_new_character和display_inventory?
我推测,因为你提到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
Run Code Online (Sandbox Code Playgroud)
这个测试会导致一些类似于以下的代码(记住,只需要足够的代码来使测试通过,不再需要)
class GameMenu
def initialize(character_creation_command)
@character_creation_command = character_creation_command
end
def execute(command)
@character_creation_command.execute
end
end
Run Code Online (Sandbox Code Playgroud)
现在我们将添加下一个测试.
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
Run Code Online (Sandbox Code Playgroud)
运行此测试将引导我们实现如下:
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
Run Code Online (Sandbox Code Playgroud)
这个实现引导我们对我们的代码提出疑问.输入无效命令时我们的代码应该怎么做?一旦我们确定了该问题的答案,我们就可以实施另一项测试.
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
Run Code Online (Sandbox Code Playgroud)
这驱逐了对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
Run Code Online (Sandbox Code Playgroud)
现在我们已经通过了测试,我们可以使用Extract Method重构将命令的验证提取到Intent Revealing方法中.
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
Run Code Online (Sandbox Code Playgroud)
现在我们终于找到了解决问题的重点.由于该invalid?方法是通过重构现有的测试代码来驱逐的,因此不需要为它编写单元测试,它已经被覆盖并且不能独立存在.由于库存和字符命令未经我们现有的测试测试,因此需要独立进行测试驱动.
请注意,我们的代码可能会更好,所以当测试通过时,让我们更清楚一点.条件语句是我们违反OCP(开放 - 封闭原则)的指标,我们可以使用替换条件多态重构来删除条件逻辑.
# 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)
!@commands.has_key? command
end
end
Run Code Online (Sandbox Code Playgroud)
现在我们重构了这个类,这样一个额外的命令只需要我们在命令哈希中添加一个额外的条目,而不是改变条件逻辑和invalid?方法.
所有的测试仍然应该通过,我们几乎完成了我们的工作.一旦我们测试了驱动单个命令,你就可以回到initialize方法并为命令添加一些默认值,如下所示:
def initialize(character_creation_command = CharacterCreation.new,
inventory_command = Inventory.new)
@commands = {
"c" => character_creation_command,
"i" => inventory_command
}
end
Run Code Online (Sandbox Code Playgroud)
最后的测试是:
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
Run Code Online (Sandbox Code Playgroud)
决赛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)
!@commands.has_key? command
end
end
Run Code Online (Sandbox Code Playgroud)
希望有所帮助!
布兰登