TDD Beyond Basics : Seams in a System - Guessing Game Kata Final Solution

Objectives


  • Learn how to find the seams in a system
  • Learn about exploratory testing

Discussion


You can use the electrical outlet to plugin a lamp, a laptop adapter, TV etc as long as the device can handle the voltage and frequency rating. There is one outlet but different devices connect to it that has different functionality. Seams in a system are similar to electrical plugin points. Why do we care about seams? Seams make the code testable and flexible.

alt text

Seam is a place where you can alter behavior of your program without editing in that place. Every seam has an enabling point, a place wehre you can make the decision to use one behavior or another.
-- Michael C. Feathers (Working Effectively with Legacy Code)

Why do we need to apply concepts that is discussed in a book about legacy code? Because it is a design technique, if you build a system with flexibility where it is needed, it becomes easy to test. You can also identify that when it is difficult to test, it is a symptom of bad design. In such cases, you need to think hard about improving the design.

The guess_game.rb still has a fake implementation for get_user_guess method:

def get_user_guess
  0
end

We now have to deal with getting input from the user. We have test driven the development of the game and we are now getting to the system boundary where the gaming engine needs to interact with users to get input. The way we interact with users is likely to change at a different rate than the game logic requirements. This means that we have found a seam in our system. This is a plugin point to our system where we could have different ways of getting user input. So, the question is : How can we abstract the standard input and standard output?

Steps


Step 1

Let's play in the irb:

irb > x = $stdin.gets
54
 => "54\n"
irb > $stdout.puts 'hi'
hi

Step 2

We can combine the standard input and standard output into a console object. By definition, console is a monitor or keyboard in a multiuser computer system. We can call this new class StandardConsole. Create a file standard_console.rb with the following contents:

class StandardConsole
  def output(message)
    puts message
  end

  def prompt(message)
    output(message)
    puts '>'
  end

  def input
    gets.chomp.to_i
  end
end

The input() method gets the user input, removes the new line and converts to an integer. Even though, StandardConsole looks like a generic class, it's not. It is specific to our guess game. So ideally we want to encapsulate this class within Gaming module.

Step 3

Change the getuserguess method in guess_game.rb as follows:

def get_user_guess
  @console.input
end

Discussion


All the specs still pass. This change was not driven by a test. If we had written an end to end test, then it would have been driven by a failing acceptance test. The same issue can also be discovered simply by playing the game in the irb. Another alternative is to use highline gem to develop a command line interface to play the guess game.

Step 4

The following is one way to refactor the code. Here is the refactored guess_game_spec.rb.

require_relative 'guess_game'

describe GuessGame do
  let(:fake_console) { double('Console').as_null_object }

  context 'Start the game' do
    it "display greeting when the game begins" do
      fake_console.should_receive(:output).with("Welcome to the Guessing Game")
      game = GuessGame.new(fake_console)
      game.start
    end

    it "prompt the user to enter the number representing their guess." do
      fake_console.should_receive(:prompt).with('Enter a number between 1 and 100')
      game = GuessGame.new(fake_console)
      game.start    
    end
  end      

  context 'Validation' do
    it "validates user guess : lower than 1" do
      game = GuessGame.new
      game.stub(:get_user_guess) { 0 }
      game.start

      game.error.should == 'The number must be between 1 and 100'            
    end

    it "validates user guess : higher than 100" do
      game = GuessGame.new
      game.stub(:get_user_guess) { 101 }
      game.start

      game.error.should == 'The number must be between 1 and 100'            
    end    
  end

  context 'Gaming Engine' do
    it "give clue : input is valid and is less than the computer pick" do
      fake_console.should_receive(:output).with('Your guess is lower')
      game = GuessGame.new(fake_console)
      game.computer_pick = 25
      game.stub(:get_user_guess) { 10 }

      game.start
    end

    it "give clue : input is valid and is greater than the computer pick" do
      fake_console.should_receive(:output).with('Your guess is higher')
      game = GuessGame.new(fake_console)
      game.computer_pick = 25
      game.stub(:get_user_guess) { 50 }

      game.start
    end

    it "recognize the correct answer when the guess is correct" do
      fake_console.should_receive(:output).with('Your guess is correct')
      game = GuessGame.new(fake_console)
      game.computer_pick = 25
      game.stub(:get_user_guess) { 25 }

      game.start
    end    
  end
end

Step 5

We realized that we need to get input from the user and provide output to the user, so having an object for StandardOutput alone is not sufficient. We need StandardInput as well. StandardOutput and StandardInput is combined into one StandardConsole object. This new object encapsulates the interaction with the standard input and standard output (monitor & keyboard).

We have modified the constructor for the guess game to reflect this bidirectional data flow with an user by using the StandardConsole. Here is the refactored guess_game.rb:

require_relative 'standard_console'
require_relative 'randomizer'

class GuessGame
  attr_accessor :computer_pick
  attr_accessor :error

  def initialize(console=StandardConsole.new, randomizer=Randomizer.new)
    @console = console
    @computer_pick = randomizer.get
  end

  def start
    @console.output("Welcome to the Guessing Game")
    @console.prompt("Enter a number between 1 and 100")
    guess = get_user_guess
    valid = validate(guess)
    give_clue if valid
  end

  def get_user_guess
    @console.input  
  end

  private

  def validate(n)
    if (n < 1) or (n > 100)
      @error = 'The number must be between 1 and 100'
      false
    else
      true
    end
  end

  def give_clue
    if get_user_guess < @computer_pick
      @console.output('Your guess is lower')
    elsif get_user_guess > @computer_pick
      @console.output('Your guess is higher')
    else
      @console.output('Your guess is correct')  
    end
  end  
end

Step 6

The following code listings remain the same as before. Here is the randomizer_spec.rb.

require_relative 'guess_game'

describe Randomizer do
  it "generate random number between 1 and 100 inclusive" do
    result = Randomizer.new.get

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

Here is the randomizer.rb

class Randomizer
  def get
    Random.new.rand(1..100)
  end
end

Here is the standard_console_spec.rb:

require_relative 'console_interface_spec'
require_relative 'standard_console'

describe StandardConsole do
  before(:each) do
    @object = StandardConsole.new
  end

  it_behaves_like "Console Interface"
end

Here is the standard_console.rb:

class StandardConsole
  def output(message)
    puts message
  end
  def prompt(message)
    output(message)
    puts ">"
  end
  def input
    gets.chomp.to_i
  end  
end

Discussion


We have found some missing abstractions and refactored our code to use the new abstractions. We can now have different implementations of the console object such as NetworkConsole, GraphicalConsole etc. Our system is extensible to different user interfacing code as long as it conforms to our console interface. Specs are more readable since they are grouped into their own context.

Step 7

The output of the specs have the puts statement output because the default console used is StandardConsole. To cleanup the output let's create a FakeConsole for testing purposes. Create fake_console.rb with the following contents:

class FakeConsole
  def output(message)
    message
  end

  def prompt(message)
    output(message + '\n' + ">")
  end  
end

Step 8

Change the guess_game_spec.rb to use the FakeConsole class to suppress the output to the standard out like this:

context 'Validation' do
  let(:game) { game = GuessGame.new(FakeConsole.new) }

  it "perform validation of the guess entered by the user : lower than 1" do
    game.stub(:get_user_guess) { 0 }
    game.start

    game.error.should == 'The number must be between 1 and 100'            
  end

  it "perform validation of the guess entered by the user : higher than 100" do
    game.stub(:get_user_guess) { 101 }
    game.start

    game.error.should == 'The number must be between 1 and 100'            
  end    
end

Step 9

Run the specs, you will see clean output like this:

GuessGame
  Start the game
    should display greeting when the game begins
    should prompt the user to enter the number representing their guess.
  Validation
    should perform validation of the guess entered by the user : lower than 1
    should perform validation of the guess entered by the user : higher than 100
  Gaming Engine
    should give clue when the input is valid and is less than the computer pick
    should give clue when the input is valid and is greater than the computer pick
    should recognize the correct answer when the guess is correct

Finished in 0.0058 seconds
7 examples, 0 failures

Step 10

Here is the actual usage of our guess game.

$irb
> load './guess_game.rb'
   => true 
> g = GuessGame.new
   => #<GuessGame:0xb0 @console=StandardConsole:0x088>, @computer_pick=42> 
> g.start
Welcome to the Guessing Game
Enter a number between 1 and 100 to guess the number
> 
> g.get_user_guess
20
Your guess is lower

> g.get_user_guess
30
Your guess is lower

> g.get_user_guess
80
Your guess is higher

> g.get_user_guess
70
Your guess is higher

> g.get_user_guess
42
Your guess is correct

Oops, you can see when we create an instance of the GuessGame class, we see the value of the number we need to guess, the value of the computer_pick, 42 in this case. We were focused on implementing the requirements. By doing exploratory testing we can find bugs or enhancements that we can implement. We experimented in the irb and saw that StandardConsole#input works. This is a change in the production code that is not driven by test.

Step 11

Let's add a to_s method to the StandardConsole and GuessGame classes so that the secret number is not revealed while playing the game. This change was driven by exploratory testing.

Add the following tos method to `guessgame.rb`:

def to_s
  "You have chosen : #{@console} to play the guess game"
end

Add the following to_s method to standard_console.rb:

def to_s
  "Standard Console"
end

Summary


In this article you learned about exploratory testing and how sometimes you make changes that is not driven by a test. You may not need a test for that change if you are confident or you feel that the consequences are not critical. You also learned about seams in a system.

Exercises


1) Use the highline gem to develop a command line interface to the guess game.

2) Play the game with Guess game and make sure you can use it's interface and it works as expected. Use any feedback to write new specs.

3) What if the client were to use the GuessGame like this :

    game = GuessGame.new
    game.play

This raises the level of abstraction and we use gaming domain specific method instead of reaching into implementation level methods.

  • What changes do you need to make for this to work?
  • Can start and get_user_guess methods be made into private methods?

Bonus


1) It would be nice to be able to say: result.should bebetween(expectedrange). Implement a custom matcher be_between for a given range.

2) Write fake_console_spec.rb that uses the shared examples to make sure it implements the abstract console interface. This will allow us to keep the FakeConsole in sync with any changes to the interface of the abstract console.

3) Version 2 of our game has to satisfy the following new requirements:

Once the user has guessed the target number correctly, you should display a report to them on their performance. This report should provide the following information:
- The target number
- The number of guesses it took the user to guess the target number
- A list of all the valid values guessed by the user in the order in which they were guessed.
- A calculated value called Cumulative error. Cumulative error is defined as the sum of the absolute value of the difference between the target number and the values guessed. For example : if the target number was 30 and the user guessed 50, 25, 35, and 30, the cumulative error would be calculated as follows:

|50-30| + |25-30| + |35-30| + |30-30| = 35

Hint: See [Abs method](http://www.w3schools.com/jsref/jsref_abs.asp "Abs method") for assistance
- A calculated value called "Average Error" which is calculated as follows: cumulative error / number of valid guesses. Using the above number set, the average error is 8.75.
- A text feedback response based on the following rules:
- If average error is 10.0 or lower, the message "Incredible guessing!"
- If average error is higher than above but under 20.0, "Good job!"
- If average error is higher than 20 but under 30.0, "Fair!"
- Anything other score: "You are horrible at this game!"


Related Articles


Create your own user feedback survey