Правильный путь к методам TDD, который вызывает другие методы - PullRequest
5 голосов
/ 28 мая 2011

Мне нужна помощь с некоторыми концепциями 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_character и display_inventory? Или я проверяю неправильные вещи в этот момент? Должен ли мой тест для метода execute только проверять, что выполнение передается правильным методам и останавливаться на этом? Тогда я должен написать больше юнит-тестов, которые специально проверяют create_new_character и display_inventory?

Ответы [ 3 ]

6 голосов
/ 29 мая 2011

Полагаю, поскольку вы упомянули TDD, рассматриваемого кода на самом деле не существует. Если это так, то вы делаете не настоящий TDD, а TAD (Test-After Development), что, естественно, приводит к таким вопросам, как этот. В 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? был исключен путем рефакторинга существующего тестируемого кода, нет необходимости писать для него модульный тест, он уже покрыт и не является самостоятельным. Поскольку команды инвентаризации и символов не проверяются в нашем существующем тесте, их нужно будет тестировать независимо.

Обратите внимание, что наш код может быть еще лучше, поэтому, пока тесты проходят, давайте немного его очистим. Условные операторы являются показателем того, что мы нарушаем OCP (открытый-закрытый принцип) , мы можем использовать рефакторинг Replace Conditional With Polymorphism для удаления условной логики.

# 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

Надеюсь, это поможет!

Brandon

3 голосов
/ 28 мая 2011

Рассмотрите возможность рефакторинга, чтобы код, отвечающий за синтаксический анализ команд (execute в вашем случае), не зависел от кода, который реализует действия (например, create_new_character, display_inventory).Это позволяет легко смоделировать действия и протестировать синтаксический анализ команды независимо. Вы хотите независимое тестирование различных образцов.

0 голосов
/ 28 мая 2011

Я бы создал нормальные тесты для create_new_character и display_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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...