Learning TDD With RSpec

Checkout Kata

This is part 1 of a planned 3 part series.

Introduction:

The challenge was to implement the code for a checkout system that handles pricing schemes such as “apples cost 50 cents, three apples cost $1.30.”

The basic guidelines on how to implement the kata can be found here

The idea is to use the Red, Green, Refactor approach.

Write a failing test, and then write little code to get the failing test to pass and then clean up the code.

Initialising project:

  1. Navigate to an appropriate directory: ~ cd practice
  2. Create a new directory & change to it: ~ mkdir checkout && cd $_
  3. Create and edit Gemfile: vim Gemfile (vim is being used as the editor for this project)
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'rspec'
  1. Press ctrl + z to navigate back to terminal and then type bundle binstubs --all (including binstubs --all after bundle is a shortcut to not use bundle exec when running the rspec command)
  2. Type bin/rspec --init to initialise rspec (a spec folder will be created)

Writing the first test:

  1. Create a new file under the spec folder: touch spec/check_out_spec.rb
  2. Write the first failing test which checks when nothing has been scanned;

    RSpec.describe 'checkout' do
    context 'when nothing has been scanned' do
      it 'shows a total of zero' do
    
        expect(total).to eq 0
      end
    end
    end
    
  3. As expected, this test fails when ⎵ + t is pressed on the keyboard;
./bin/rspec spec/check_out_spec.rb

Randomized with seed 59413
F

Failures:

  1) checkout when nothing has been scanned shows a total of zero
     Failure/Error: expect(total).to eq 0

     NameError:
       undefined local variable or method `total' for #<RSpec::ExampleGroups::Checkout::WhenNothingHasBeenScanned "shows a total of zero" (./spec/check_out_spec.rb:3)>
     # ./spec/check_out_spec.rb:5:in `block (3 levels) in <top (required)>'

Finished in 0.00169 seconds (files took 0.04863 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/check_out_spec.rb:3 # checkout when nothing has been scanned shows a total of zero

Randomized with seed 59413


shell returned 1

Press ENTER or type command to continue
  1. Let us incrementally get this test to pass. The feedback loop with TDD is second to none as the error actually tells us what to do next

    RSpec.describe 'checkout' do
    context 'when nothing has been scanned' do
     it 'shows a total of zero' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)
       total = checkout.total
    
       expect(total).to eq 0
     end
    end
    end
    

    In the above code snippet, two lines have been added; pricing_rules = :Rule and checkout = CheckOut.new(pricing_rules)

The error is now different;

~
./bin/rspec spec/check_out_spec.rb

Randomized with seed 59719
F

Failures:

  1) checkout when nothing has been scanned shows a total of zero
     Failure/Error: checkout = CheckOut.new(pricing_rules)

     NameError:
       uninitialized constant CheckOut
     # ./spec/check_out_spec.rb:5:in `block (3 levels) in <top (required)>'

Finished in 0.00174 seconds (files took 0.04954 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/check_out_spec.rb:3 # checkout when nothing has been scanned shows a total of zero

Randomized with seed 59719


shell returned 1

Press ENTER or type command to continue

Now we create add a class: CheckOut as the code below;

 class CheckOut

 end

 RSpec.describe 'checkout' do
   context 'when nothing has been scanned' do
     it 'shows a total of zero' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)
       total = checkout.total

       expect(total).to eq 0
     end
   end
 end

Now, this is the error returned;

~
./bin/rspec spec/check_out_spec.rb

Randomized with seed 45459
F

Failures:

  1) checkout when nothing has been scanned shows a total of zero
     Failure/Error: checkout = CheckOut.new(pricing_rules)

     ArgumentError:
       wrong number of arguments (given 1, expected 0)
     # ./spec/check_out_spec.rb:9:in `initialize'
     # ./spec/check_out_spec.rb:9:in `new'
     # ./spec/check_out_spec.rb:9:in `block (3 levels) in <top (required)>'

Finished in 0.00264 seconds (files took 0.05216 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/check_out_spec.rb:7 # checkout when nothing has been scanned shows a total of zero

Randomized with seed 45459


shell returned 1

Press ENTER or type command to continue

Now we can write code to solve the above error. It would look something like so;

 class CheckOut
   def initialize(pricing_rules)

   end

 end

 RSpec.describe 'checkout' do
   context 'when nothing has been scanned' do
     it 'shows a total of zero' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)
       total = checkout.total

       expect(total).to eq 0
     end
   end
 end

For this, we get the undefined method total as the error shows;

~
./bin/rspec spec/check_out_spec.rb

Randomized with seed 46712
F

Failures:

  1) checkout when nothing has been scanned shows a total of zero
     Failure/Error: total = checkout.total

     NoMethodError:
       undefined method `total' for #<CheckOut:0x000000013952bd60>
     # ./spec/check_out_spec.rb:13:in `block (3 levels) in <top (required)>'

Finished in 0.00174 seconds (files took 0.05301 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/check_out_spec.rb:10 # checkout when nothing has been scanned shows a total of zero

Randomized with seed 46712


shell returned 1

Press ENTER or type command to continue

To fix the error, add a method: total as shown below;

class CheckOut
  def initialize(pricing_rules)

  end

  def total

  end
end

RSpec.describe 'checkout' do
  context 'when nothing has been scanned' do
    it 'shows a total of zero' do
      pricing_rules = :Rule
      checkout = CheckOut.new(pricing_rules)
      total = checkout.total

      expect(total).to eq 0
    end
  end
end

We now get another error as shown;

~
./bin/rspec spec/check_out_spec.rb

Randomized with seed 15684
F

Failures:

  1) checkout when nothing has been scanned shows a total of zero
     Failure/Error: expect(total).to eq 0

       expected: 0
            got: nil

       (compared using ==)
     # ./spec/check_out_spec.rb:18:in `block (3 levels) in <top (required)>'

Finished in 0.00714 seconds (files took 0.05137 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/check_out_spec.rb:13 # checkout when nothing has been scanned shows a total of zero

Randomized with seed 15684


shell returned 1

Press ENTER or type command to continue

Now, to get this to pass, we need to change the total method to return 0;


class CheckOut
  def initialize(pricing_rules)

  end

  def total
    0
  end
end

RSpec.describe 'checkout' do
  context 'when nothing has been scanned' do
    it 'shows a total of zero' do
      pricing_rules = :Rule
      checkout = CheckOut.new(pricing_rules)
      total = checkout.total

      expect(total).to eq 0
    end
  end
end

Below is the output of the passing test;

~
./bin/rspec spec/check_out_spec.rb

Randomized with seed 9210
.

Finished in 0.00187 seconds (files took 0.04874 seconds to load)
1 example, 0 failures

Randomized with seed 9210


Press ENTER or type command to continue

This is just one of the tests though. Other tests need to be done i.e. when A has been scanned, when B has been scanned, when C has been scanned, when D has been scanned

This concludes part 1.

In part 2, we will tackle how to handle tests for the scanning of A, B, C & D.

In part 3, we will do some refactoring to make the code better.

Shout out to Rob for his immense assistance on this and making it easy to comprehend.