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

时间:2011-05-28 09:30:16

标签: 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 display inventory
end

现在我不确定要为我的单元测试编写什么。如果我为execute方法编写单元测试并不能完全覆盖我对create_new_characterdisplay_inventory的测试吗?或者我在那时测试错误的东西?我对execute方法的测试是否只测试执行是否传递给正确的方法并停在那里?那么我应该编写更多专门测试create_new_characterdisplay_inventory的单元测试吗?

3 个答案:

答案 0 :(得分: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

现在我们已经通过了测试,我们可以使用 Extract Method 重构将命令验证提取到 Intent Revealing Method

  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?方法是通过重构现有的测试代码来驱逐的,因此不需要为它编写单元测试,它已经被覆盖并且不能独立存在。由于库存和字符命令未经我们现有测试测试,因此需要独立测试驱动。

请注意,我们的代码可能会更好,所以当测试通过时,让我们将其清理一下。条件语句表明我们违反了 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

现在我们重构了这个类,这样一个额外的命令只需要我们在命令哈希中添加一个额外的条目,而不是改变我们的条件逻辑以及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)
    !@commands.has_key? command
  end
end

希望有所帮助!

布兰登

答案 1 :(得分:3)

考虑重构,以便负责解析命令的代码(在您的情况下为execute)独立于实现操作的代码(即create_new_characterdisplay_inventory)。这样可以轻松地模拟操作并独立测试命令解析。 希望对不同部分进行独立测试。

答案 2 :(得分:0)

我会为create_new_characterdisplay_inventory创建常规测试,最后测试execute,只是一个包装函数,设置期望来检查是否调用了适当的命令(以及结果返回)。这样的事情:

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