5

I need some help with some TDD concepts. Say I have the following code

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

Now I'm not sure what to write my unit tests for. If I write unit tests for the execute method doesn't that pretty much cover my tests for create_new_character and display_inventory? Or am I testing the wrong stuff at that point? Should my test for the execute method only test that execution is passed off to the correct methods and stop there? Then should I write more unit tests that specifically test create_new_character and display_inventory?

Dty
  • 12,253
  • 6
  • 43
  • 61

3 Answers3

6

I'm presuming since you mention TDD the code in question does not actually exist. If it does then you aren't doing true TDD but TAD (Test-After Development), which naturally leads to questions such as this. In TDD we start with the test. It appears that you are building some type of menu or command system, so I'll use that as an example.

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

This test would lead to some code similar to the following (remember, just enough code to make the test pass, no more)

class GameMenu
  def initialize(character_creation_command)
    @character_creation_command = character_creation_command
  end

  def execute(command)
    @character_creation_command.execute
  end
end

Now we'll add the next test.

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

Running this test will lead us to an implementation such as:

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

This implementation leads us to a question about our code. What should our code do when an invalid command is entered? Once we decide the answer to that question we could implement another test.

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

That drives out a quick change to the execute method

  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

Now that we have passing tests we can use the Extract Method refactoring to extract the validation of the command into an 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

Now we finally got to the point we can address your question. Since the invalid? method was driven out by refactoring existing code under test then there is no need to write a unit test for it, it's already covered and does not stand on it's own. Since the inventory and character commands are not tested by our existing test, they will need to be test driven independently.

Note that our code could be better still so, while the tests are passing, lets clean it up a bit more. The conditional statements are an indicator that we are violating the OCP (Open-Closed Principle) we can use the Replace Conditional With Polymorphism refactoring to remove the conditional logic.

# 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

Now we've refactored the class such that an additional command simply requires us to add an additional entry to the commands hash rather than changing our conditional logic as well as the invalid? method.

All the tests should still pass and we have almost completed our work. Once we test drive the individual commands you can go back to the initialize method and add some defaults for the commands like so:

  def initialize(character_creation_command = CharacterCreation.new,
                 inventory_command = Inventory.new)
    @commands = {
      "c" => character_creation_command,
      "i" => inventory_command
    }
  end

The final test is:

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

And the final GameMenu looks like:

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

Hope that helps!

Brandon

bcarlso
  • 2,345
  • 12
  • 12
  • thanks for the detailed answer. You gave me a lot to chew on and think about. The only thing that's really bugging me about your example is that the GameMenu initializer will get really long after adding a lot of commands. And testing it will be easy to mess up if I have to keep track that my new "show map" command is say 10 parameters down the list. Any good solution for this? – Dty May 29 '11 at 12:23
  • @Dty Absolutely. I had considered that. I figured that for this small of an example it wouldn't be that big of a deal, but you confirmed that it is/could be. There are a couple of ways you could deal with it. The first one that comes to mind is to add a register_menu_command that could be called externally to register the commands. The second would be replace that parameter list with the _Builder Pattern_ and simply pass in a MenuBuilder that generates the hash. You could configure the builder in your test. I would likely prefer the builder solution. – bcarlso May 29 '11 at 17:45
  • I'm still worried this solution is a bit over engineered but I learned a lot from it and it definitely answers my question. Thanks! – Dty Jun 01 '11 at 00:34
3

Consider refactoring so that the code that has responsibility for parsing commands (execute in your case) is independent of the code that implements the actions (i.e., create_new_character, display_inventory). That makes it easy to mock the actions out and test the command parsing independently. You want independent testing of the different pieces.

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • I'm not sure I understand what you mean. For example, I already feel the parsing of commands and execution of actions are independent. Could you show me a short code sample of what you mean? Perhaps that will help me understand. – Dty May 28 '11 at 10:17
0

I would create normal tests for create_new_character and display_inventory, and finally to test execute, being just a wrapper function, set expectations to check that the apropriate command is called (and the result returned). Something like that:

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
tokland
  • 66,169
  • 13
  • 144
  • 170