TDD Basics : Bowling Game Kata

Objectives


  • Learn why you should not make changes to both production code and specs and then run specs.
  • Learn how to find abstractions in the domain to make your code expressive.

Difficulty Level


Difficult

Discussion


We will use the scoring rules found at http://www.bowling2u.com/trivia/game/scoring.asp for our specs. You can also refer the rules Scoring Bowling.html at https://github.com/bparanj/polgar/tree/master/13-bowling-game-gem

Steps


Step 1

Create a filebowling_game_spec.rb with the following contents:

describe BowlingGame do
end

Step 2

Run the spec:

$rspec bowling_game_spec.rb --color

You get the error:

uninitialized constant Object::BowlingGame (NameError)

Step 3

Add the following code to the top of the bowling_game_spec.rb:

class BowlingGame
end

describe BowlingGame do
end

Step 4

Run the spec :

$rspec bowling_game_spec.rb --color
No examples found.

Finished in 0.00005 seconds
0 examples, 0 failures

Step 5

Let's write our first spec:

class BowlingGame
end

describe BowlingGame do
  it 'scores all gutters with 0'
end

Step 6

When you run the spec, you now get the output:

BowlingGame
  scores all gutters with 0 (PENDING: Not yet implemented)

Pending:
  BowlingGame scores all gutters with 0
    # Not yet implemented
    # ./bowling_game_spec.rb:6

Finished in 0.00029 seconds
1 example, 0 failures, 1 pending

Step 7

You can use the it block to get things out of your head as a to do list to implement. Add another spec:

class BowlingGame
end

describe BowlingGame do
  it 'scores all gutters with 0'
  it "scores all 1's with 20"
end

Step 8

When you run the specs, you now get the output:

BowlingGame
  scores all gutters with 0 (PENDING: Not yet implemented)
  scores all 1's with 20 (PENDING: Not yet implemented)

Pending:
  BowlingGame scores all gutters with 0
    # Not yet implemented
    # ./bowling_game_spec.rb:6
  BowlingGame scores all 1's with 20
    # Not yet implemented
    # ./bowling_game_spec.rb:7

Finished in 0.0003 seconds
2 examples, 0 failures, 2 pending

Step 9

Let's now express our first requirement as follows:

class BowlingGame
end

describe BowlingGame do
  it 'scores all gutters with 0' do
    game = BowlingGame.new

    20.times { game.roll(0) }

    expect(game.score).to eq(0)
  end

  it "scores all 1's with 20"
end

Step 10

When you run the specs, you now get the output:

1) BowlingGame scores all gutters with 0
   Failure/Error: 20.times { game.roll(0) }
   NoMethodError:
     undefined method `roll' for BowlingGame

The test is not failing for the right reason because it is due to an error in not defining roll method. We are in yellow state.

Step 11

Let's do the minimal thing to get past this error message by defining the roll() method in BowlingGame class :

class BowlingGame
  def roll
  end
end

describe BowlingGame do
  # same code as before
end

Step 12

When you run the specs, you now get the output:

1) BowlingGame scores all gutters with 0
   Failure/Error: def roll
   ArgumentError:
     wrong number of arguments (1 for 0)

The test is not failing for the right reason because it is due to an error in the definition of roll method. We are in yellow state.

Step 13

Let's do the minimal thing required to get past this error by changing the roll method to take an input argument:

class BowlingGame
  def roll(number)

  end
end

describe BowlingGame do
  # same code as before
end

Step 14

When you run the specs, you now get the output:

1) BowlingGame scores all gutters with 0
    Failure/Error: expect(game.score).to eq(0)
    NoMethodError:
      undefined method `score' for BowlingGame

The test is not failing for the right reason because it is due to an error in the syntax of score() method. We are in yellow state.

Step 15

Let's define a score method for the BowlingGame class.

class BowlingGame
  def roll(number)

  end

  def score

  end
end

describe BowlingGame do
  # same code as before
end

Step 16

When you run the specs, you now get the output:

1) BowlingGame scores all gutters with 0
    Failure/Error: expect(game.score).to eq(0) 
      expected: 0
           got: nil

Our test is failing for the right reason. We are now in red state.

Step 17

Let's change the score method like this:

class BowlingGame
  def roll(number)

  end

  def score
    0    
  end
end

describe BowlingGame do
  # same code as before
end

Step 18

When you run the specs, you now get the output:

BowlingGame
  scores all gutters with 0
  scores all 1's with 20 (PENDING: Not yet implemented)

Pending:
  BowlingGame scores all 1's with 20
    # Not yet implemented
    # ./bowling_game_spec.rb:20

Finished in 0.00138 seconds
2 examples, 0 failures, 1 pending

The first spec now passes. We are now green.

Step 19

Let's now express our second requirement:

class BowlingGame
  def roll(number)

  end

  def score
    0    
  end
end

describe BowlingGame do
  it 'scores all gutters with 0' do
  # same code as before
  end

  it "scores all 1's with 20" do
    game = BowlingGame.new

    20.times { game.roll(1) }

    expect(game.score).to eq(20)    
  end
end

Step 20

When you run the specs, you now get the output:

1) BowlingGame scores all 1 s with 20
    Failure/Error: expect(game.score).to eq(20)

      expected: 20
           got: 0

The spec is failing for the right reason.

Step 21

Change the implementation as follows:

class BowlingGame
  def initialize
    @score = 0
  end

  def roll(number)
    @score += number
  end

  def score
    @score
  end
end

describe BowlingGame do
  # same code as before
end

Step 22

When you run the specs, you now get the output:

BowlingGame
  scores all gutters with 0
  scores all 1 s with 20

Finished in 0.00145 seconds
2 examples, 0 failures

We are now green. We did not go to the yellow state before we went green. This is ok.

Step 23

Let's cleanup our BowlingGame class like this:

class BowlingGame
  attr_reader :score

  def initialize
    @score = 0
  end
  def roll(number)
    @score += number
  end
end

describe BowlingGame do
  # same code as before
end

All the specs still pass.

Step 24

Now there is no duplication in the production code, but what can we do to make it more expressive of the domain? Let's rename the number argument to pin like this:

class BowlingGame
  attr_reader :score

  def initialize
    @score = 0
  end
  def roll(pins)
    @score += pins
  end
end

describe BowlingGame do
  # same code as before
end

The specs still passes since it does not depend on this implementation detail.

Step 25

Let's move the BowlingGame class into its own file bowling_game.rb. Add the require_relative to the top of the bowling_game_spec.rb:

require_relative 'bowling_game'

describe BowlingGame do
  # same code as before
end

Run the specs again. It should pass. We cleaned up one thing after another, we were green before refactoring and ended in green after refactoring. Why should we not refactor in red state? Refer the appendix of the Essential TDD book for the answer.

Step 26

Now let's look the spec and see if we can refactor. The refactored specs look like this:

require_relative 'bowling_game'

describe BowlingGame do
  before do
    @game = BowlingGame.new
  end

  it 'scores all gutters with 0' do
    20.times { @game.roll(0) }

    expect(@game.score).to eq(0)
  end

  it "scores all 1's with 20" do
    20.times { @game.roll(1) }

    expect(@game.score).to eq(20)    
  end
end

The specs still pass. In the tests where we are looping 20 times, this test is taken from the rspec home page example for bowling game. In the next article we will see why this is a bad idea and how to refactor it to a better solution.

Exercise


Replace the before() method with let() method and make all the specs pass.

Discussion


Do you always need to take small steps when writing tests?

If you notice, we change either production code or the spec but not both at the same time. Regardless of whether we are refactoring or driving the design of our code. If we change both the spec and the production code at the same time we will not know which file is causing the problem. If we take small steps, we will be able to immediately fix the problem. Because we know what we just changed. If you are confident you can take bigger steps as long as the duration of the red state is minimum and you get to green and stay in green longer.

alt text

Summary


In this article you learned about why you need to run specs as soon as you make changes to either the production code or the specs. You also learned how to find abstractions to make your code expressive. In this example, it was a simple change in the name of the variable to make it intention revealing. In the next article we will continue working on the bowling game to discuss more topics.


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.