Ruby 调用其他方法的TDD方法的正确方法

Ruby 调用其他方法的TDD方法的正确方法,ruby,tdd,Ruby,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 disp

我需要一些关于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\u new\u character
display\u inventory
的测试吗?还是我在测试错误的东西?我对
execute
方法的测试是否应该只测试执行是否传递到正确的方法并停止?然后,我是否应该编写更多的单元测试,专门测试
创建新的\u字符
显示清单

考虑重构,以便负责解析命令的代码(
在您的情况下执行
)独立于实现操作的代码(即,
创建新字符
显示清单
)。这使得模拟操作和独立测试命令解析变得很容易。您需要对不同的部分进行独立测试。

我将为
创建新字符
显示清单
创建正常测试,最后测试
执行
,作为一个包装函数,设置要检查的期望值调用适当的命令(并返回结果)。类似于:

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

我想,既然你提到了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
此测试将生成类似于以下内容的代码(请记住,只需要足够的代码即可通过测试,仅此而已)

现在我们将添加下一个测试

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
现在我们已经通过了测试,我们可以使用提取方法重构将命令的验证提取到一个意图揭示方法中

现在,我们终于可以解决您的问题了。由于
无效?
方法是通过重构测试中的现有代码而产生的,因此不需要为其编写单元测试,它已经被涵盖,并且不独立于它自己。由于库存和字符命令没有通过我们现有的测试进行测试,因此它们可以我需要独立进行测试

请注意,我们的代码可能会更好,因此,当测试通过时,让我们再清理一下。条件语句表明我们违反了OCP(开-闭原则)。我们可以使用Replace conditional With polymorphics重构来删除条件逻辑

# 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
现在我们已经重构了这个类,这样一个额外的命令只需要我们在命令散列中添加一个额外的条目,而不是改变我们的条件逻辑以及
无效?
方法

所有的测试都应该通过,我们几乎已经完成了我们的工作。一旦我们测试了各个命令,您就可以返回到initialize方法并为这些命令添加一些默认值,如下所示:

  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
最后的
游戏菜单如下所示:

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
希望有帮助


Brandon

我不确定我是否理解你的意思。例如,我已经觉得命令的解析和动作的执行是独立的。你能给我一个简短的代码示例来说明你的意思吗?也许这会帮助我理解。谢谢你的详细回答。你给了我很多思考和思考的机会。唯一真正有用的是你的例子让我感到困扰的是,在添加了很多命令之后,GameMenu初始值设定项会变得很长时间。如果我不得不跟踪我的新“show map”,那么测试很容易出错命令是列表下面的10个参数。有什么好的解决方案吗?@Dty绝对有。我已经考虑过了。我认为对于这个小例子来说,这不会有什么大不了的,但您确认了它是/可能是。有几种方法可以处理它。首先想到的是添加一个
register\u menu\u command
可以在外部调用以注册命令。第二种方法是用生成器模式替换该参数列表,只需传入一个生成哈希的MenuBuilder。您可以在测试中配置生成器。我可能更喜欢生成器解决方案。我仍然担心此解决方案过于工程化我从中学到了很多,它肯定回答了我的问题。谢谢!
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
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