TDD Beyond Basics : Guessing Game Kata Part 2

Objective


Problem Statement


Refer the guessing game problem description described in the previous article : TDD Beyond Basics : Testing Random Behavior

Steps


Step 1

Let's now write the second example. Here is the guess_game_spec.rb:

require_relative 'guess_game'

describe GuessGame do
  # other specs remain the same as before
  it 'display greeting when the game begins' do
    fake_console = mock('Console')
    fake_console.should_receive(:output).with('Welcome to the Guessing Game')

    game = GuessGame.new(fake_console)

    game.start
  end
end

Step 2

Run the spec, you will see : undefined method 'start' error message.

Step 3

Let's write the minimal code required to get past the error message.

class GuessGame
  # other code same as before
  def start

  end
end

Step 4

We have defined an empty start method. Run the specs again, you will see.

1) GuessGame should display greeting when the game begins
Failure/Error: 
fake_console.should_receive(:output).with("Welcome to the Guessing Game")
  (Mock "Console").output("Welcome to the Guessing Game")
  expected: 1 time
  received: 0 times

This test is failing because the console object never received the output(string) method call.

Step 5

Change the GuessGame class as follows:

class GuessGame
  def initialize(console)
    @console = console
  end

  def random
    Random.new.rand(1..100)
  end

  def start
    @console.output('Welcome to the Guessing Game')
  end
end

GuessGame class now has a constructor that takes a console object. It then delegates welcoming the user to the console object in the start method. This is an example of dependency injection. Any collaborator that conforms to the interface we have discovered can be used to construct a GuessGame object.

Step 6

Run the specs again, you will see the following failure message:

1) GuessGame should generate random number between 1 and 100 inclusive 
    Failure/Error: game = GuessGame.new
    ArgumentError:
      wrong number of arguments (0 for 1)

This implementation broke our previous test which is not passing in the console object to the constructor. We can fix it by initializing the default value to standard output.

Step 7

Change the guess_game.rb constructor as follows:

class GuessGame
  def initialize(console=STDOUT)
    @console = console
  end
end

Step 8

Run the specs now. It will pass. We are back to green. This spec shows how you can defer decisions about how to interact with the user. It could be standard output, GUI, client server app etc. Fake object is injected into the game object.

Here is the complete listing for this lesson:

require_relative 'guess_game'

describe GuessGame do
  it 'generates random number between 1 and 100 inclusive' do
    game = GuessGame.new
    result = game.random

    expected = 1..100
    expected.should cover(result)
  end

  it 'displays greeting when the game begins' do
    fake_console = double('Console')
    fake_console.should_receive(:output).with('Welcome to the Guessing Game')
    game = GuessGame.new(fake_console)
    game.start
  end
end
class GuessGame
  def initialize(console=STDOUT)
    @console = console
  end

  def random
    Random.new.rand(1..100)
  end

  def start
    @console.output('Welcome to the Guessing Game')
  end
end

Discussion


The public interface output(string) of the Console object is discovered during the mocking step. It hides the details about the type of interface that must be implemented to communicate with an user. Game delegates any user interfacing code to a collaborating console object therefore it obeys Single Purpose Principle. Console object also obeys the Single Purpose Principle by focusing only on one concrete implementation of dealing with user interaction.

We could have implemented this similar to the code breaker game in The RSpec book by calling the puts method on output variable.

module Codebreaker
  class Game
    def initialize(output)
      @output = output
    end
    def start
      @output.puts("Welcome to Codebreaker!")
    end
  end
end

module Codebreaker
  describe Game do
    describe "#start" do
      it "sends a welcome message" do
        output = double('output')
        game = Game.new(output)
        output.should_receive(:puts).with('Welcome to Codebreaker!')
        game.start
      end
      it "prompts for the first guess"
    end
  end
end

By doing so we tie our game object to the implementation details. This results in tightly coupled objects which is not desirable. Whenever we change the way we interface with the external world, the code will break. We desire loosely coupled objects with high cohesion.

You might encounter a problem where random number generation spec fails when user interfacing feature is modified. Random number generation and user interfacing logic are not related in any way. Ideally they should be split into separate classes that has only one purpose. We will revisit this topic later.

Summary


In this lesson you learned how to apply Single Purpose principle and how to design collaborators that are highly cohesive. We also saw some bad code examples in RSpec book that ties implementation level details with game logic. In the next lesson we will continue building the guessing game.

Reference


Everything that is wrong with mocking, bdd, and rspec
Single Purpose Principle


Related Articles


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.