TDD Beyond Basics : Guessing Game Kata Part 2
- To apply Single Purpose Principle to Guessing Game Kata
Refer the guessing game problem description described in the previous article : TDD Beyond Basics : Testing Random Behavior
Let's now write the second example. Here is the
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
Run the spec, you will see : undefined method 'start' error message.
Let's write the minimal code required to get past the error message.
class GuessGame # other code same as before def start end end
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.
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.
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.
Change the guess_game.rb constructor as follows:
class GuessGame def initialize(console=STDOUT) @console = console end end
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
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.
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.