Skip to main content

Learning TDD With RSpec Part 3

Checkout Kata Part 3

Published 8 min readMarkdown
Learning TDD With RSpec Part 3

This is part 3 (the last) in this series.

Introduction:

In part 2, this was achieved;

  1. Scanning of each of the items, A, B, C, and D was done
  2. The corresponding test was written for each

In part 3, this is going to be handled;

  1. Some house cleaning
  2. Changing CheckOut Spec
  3. Adding PricingRules Spec
  4. Adding PricingRules Class
  5. Adding pricing rules

Some House Cleaning:

The code in the ChecKOut class needs to be included in a separate file under lib/checkout.rb

Below is the checkout.rb file:

class CheckOut
  def initialize(pricing_rules)
    @total = 0
  end

  def total
    @total
  end

  def scan(item)
    if item == 'A'
      @total = 50
    elsif item == 'B'
      @total = 30
    elsif item == 'C'
      @total = 20
    else
      @total = 15
    end
  end
end

The checkout spec needs to included in a separate file under spec/checkout_spec.rb

Below is the checkout_spec.rb file:

require 'checkout'

 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
   context 'when A has been scanned' do
     it 'shows the price of A' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)

       checkout.scan('A')
       total = checkout.total

       expect(total).to eq 50
     end
   end
   context 'when B has been scanned' do
     it 'shows the price of B' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)

       checkout.scan('B')
       total = checkout.total

       expect(total).to eq 30
     end
   end
   context 'when C has been scanned' do
     it 'shows the price of C' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)

       checkout.scan('C')
       total = checkout.total

       expect(total).to eq 20
     end
   end
   context 'when D has been scanned' do
     it 'shows the price of D' do
       pricing_rules = :Rule
       checkout = CheckOut.new(pricing_rules)

       checkout.scan('D')
       total = checkout.total

       expect(total).to eq 15
     end
   end
 end

Changing CheckOut Spec:

This is the new checkout_spec.rb file:

require 'checkout'
require 'pricing_rules'

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

      expect(total).to eq 0
    end
  end

  context 'when A has been scanned' do·
    it 'shows the price of A' do
      pricing_rules = PricingRules.new(
        "A" => {unit_price: 50, discount_price: 130, discount_count: 3})
      checkout = CheckOut.new(pricing_rules)
      checkout.scan('A')
      total = checkout.total

      expect(total).to eq 50
    end
  end

  context 'when B has been scanned' do
    it 'shows the price of B' do
      pricing_rules = PricingRules.new(
        "B" => {unit_price: 30, discount_price: 45, discount_count: 2})
      checkout = CheckOut.new(pricing_rules)
      checkout.scan('B')
      total = checkout.total

      expect(total).to eq 30
    end
  end

  context 'when C has been scanned' do·
    it 'shows the price of C' do
      pricing_rules = PricingRules.new("C" => {unit_price: 20})
      checkout = CheckOut.new(pricing_rules)
      checkout.scan('C')
      total = checkout.total

      expect(total).to eq 20
    end
  end

  context 'when D has been scanned' do·
    it 'shows the price of D' do
      pricing_rules = PricingRules.new("D" => {unit_price: 15})
      checkout = CheckOut.new(pricing_rules)
      checkout.scan('D')
      total = checkout.total

      expect(total).to eq 15
    end
  end
 end

This is the new checkout.rb file:

 class CheckOut
   def initialize(pricing_rules)
     @pricing_rules = pricing_rules
     @items_count = Hash.new(0)·
   end

   def scan(item)
     @items_count[item] += 1
   end

   def total
     @items_count.inject(0) do |total, (item, count)|
       if item == 'B' || item == 'A'
         total += @pricing_rules.price_for(item, count)
       else
         total += count*@pricing_rules.unit_price(item)
       end
     end
   end
 end

Adding PricingRules Spec:

This is the pricing_rules_spec.rb file;

 require "pricing_rules"

 RSpec.describe PricingRules do
   describe "#unit_price" do
     it "fetches the unit price for the item" do
       pricing_rules = PricingRules.new("A" => {unit_price: 50})

       unit_price = pricing_rules.unit_price("A")

       expect(unit_price).to eq 50
     end
   end

   describe "#discount_price" do
     it "fetches the discount price for the item" do
       pricing_rules = PricingRules.new("A" => {discount_price: 130})

       discount_price = pricing_rules.discount_price("A")

       expect(discount_price).to eq 130
     end
   end

   describe "#discount_count" do
     it "fetches the discount count of the item" do
       pricing_rules = PricingRules.new("A" => {discount_count: 3})

       discount_count = pricing_rules.discount_count("A")

       expect(discount_count).to eq 3
     end
   end

   describe "#price_for" do
     it "calculates the price for a number of items" do

       pricing_rules = PricingRules.new(
         "A" => {unit_price: 50, discount_price: 130, discount_count: 3})

       price = pricing_rules.price_for("A", 3)

      expect(price).to eq 130
    end
  end

  describe "#has_dicount_price" do
    it "checks if an item has a discount price" do
      pricing_rules = PricingRules.new("A" => {discount_price: 130})

      has_discount = pricing_rules.has_discount_price("A")

      expect(has_discount).to be(true)
    end
  end
end

Adding PricingRules Class:

This is the pricing_rules.rb file:

 class PricingRules·
   def initialize(rules)
     @rules = rules
   end

   def unit_price(item)
     @rules[item][:unit_price]
   end

   def discount_price(item)
     @rules[item][:discount_price]
   end

   def discount_count(item)
     @rules[item][:discount_count]
   end

   def price_for(item, count)
     count/discount_count(item) * discount_price(item) +·
     count% discount_count(item) * unit_price(item)·
   end

   def has_discount_price(item)
     @rules[item].has_key?(:discount_price)
   end
 end

When the pricing_rules_spec tests are run, they pass successfully;

./bin/rspec spec/pricing_rules_spec.rb:3
Run options: include {:locations=>{"./spec/pricing_rules_spec.rb"=>[3]}}

Randomized with seed 46467

PricingRules
  #has_dicount_price
    checks if an item has a discount price
  #price_for
    calculates the price for a number of items
  #discount_price
    fetches the discount price for the item
  #discount_count
    fetches the discount count of the item
  #unit_price
    fetches the unit price for the item

Finished in 0.0013 seconds (files took 0.05617 seconds to load)
5 examples, 0 failures

Randomized with seed 46467


Press ENTER or type command to continue

Adding Pricing Rules:

Edit the checkout.rb file to include pricing_rules in the initialize method.

The checkout.rb file changes to:

class CheckOut
  def initialize(pricing_rules)
    @pricing_rules = pricing_rules
    @total = 0
  end

  def total
    @total
  end

  def scan(item)
    if item == 'A'
      @total = 50
    elsif item == 'B'
      @total = 30
    elsif item == 'C'
      @total = 20
    else·
      @total = 15
    end
  end
end

Make certain that written tests still pass and they do as shown below:

./bin/rspec spec/check_out_spec.rb

Randomized with seed 13654
.....

Finished in 0.00245 seconds (files took 0.05355 seconds to load)
5 examples, 0 failures

Randomized with seed 13654


Press ENTER or type command to continue

Edit the chekout.rb file further to this;

class CheckOut
  def initialize(pricing_rules)
    @pricing_rules = pricing_rules
    @items_count = Hash.new(0)·
  end
··
  def scan(item)
    @items_count[item] += 1
  end

  def total
    @items_count.inject(0) do |total, (item, count)|
      if item == 'B' || item == 'A'
        total += @pricing_rules.price_for(item, count)
      else
        total += count*@pricing_rules.unit_price(item)
      end
    end
  end
end

This was done to take into account the pricing_rules and items_count

Edit the checkout_spec.rb file to gradually include pricing rules information

Adding Pricing Rules To A

Include pricing_rules for the context 'when A has been scanned':

context 'when A has been scanned' do
  it 'shows the price of A' do
    pricing_rules = PricingRules.new(
      "A" => {unit_price: 50, discount_price: 130, discount_count: 3})
    checkout = CheckOut.new(pricing_rules)

    checkout.scan('A')
    total = checkout.total

    expect(total).to eq 50
  end
end

When the test is run, it passes successfully:

./bin/rspec spec/checkout_spec.rb:16
Run options: include {:locations=>{"./spec/checkout_spec.rb"=>[16]}}

Randomized with seed 64567

checkout
  when A has been scanned
    shows the price of A

Finished in 0.00064 seconds (files took 0.04095 seconds to load)
1 example, 0 failures

Randomized with seed 64567


Press ENTER or type command to continue

Adding Pricing Rules to the rest of the items:

   context 'when B has been scanned' do
     it 'shows the price of B' do
       pricing_rules = PricingRules.new(
         "B" => {unit_price: 30, discount_price: 45, discount_count: 2})
       checkout = CheckOut.new(pricing_rules)
       checkout.scan('B')
       total = checkout.total

       expect(total).to eq 30
     end
   end

   context 'when C has been scanned' do·
     it 'shows the price of C' do
       pricing_rules = PricingRules.new("C" => {unit_price: 20})
       checkout = CheckOut.new(pricing_rules)
       checkout.scan('C')
       total = checkout.total

       expect(total).to eq 20
     end
   end

   context 'when B, A and then D have been scanned' do
     it 'shows the total price of B, A and D' do
       pricing_rules = PricingRules.new(
         "B" => {unit_price: 30, discount_price: 45, discount_count: 2},
         "A" => {unit_price: 50, discount_price: 130, discount_count: 3},
         "D" => {unit_price: 15}
       )
       checkout = CheckOut.new(pricing_rules)

       checkout.scan('B')
       checkout.scan('A')
       checkout.scan('D')
       total = checkout.total

       expect(total).to eq 95
     end
   end

  context 'when D has been scanned' do·
    it 'shows the price of D' do
      pricing_rules = PricingRules.new("D" => {unit_price: 15})
      checkout = CheckOut.new(pricing_rules)
      checkout.scan('D')
      total = checkout.total

      expect(total).to eq 15
    end
  end

  context 'when B, A and another B has been scanned' do
    it 'shows the total discounted price of B plus price of A' do
      pricing_rules = PricingRules.new(
        "B" => {unit_price: 30, discount_price: 45, discount_count: 2},
        "A" => {unit_price: 50, discount_price: 130, discount_count: 3},
        "D" => {unit_price: 15}
      )
      checkout = CheckOut.new(pricing_rules)

      checkout.scan('B')
      checkout.scan('A')
      checkout.scan('B')
      total = checkout.total

      expect(total).to eq 95
    end
  end

  context 'when A, A, and another A have been scanned' do
    it 'shows the total discounted price of three A items' do
      pricing_rules = PricingRules.new(
        "A" => {unit_price: 50, discount_price: 130, discount_count: 3})
      checkout = CheckOut.new(pricing_rules)

      checkout.scan('A')
      checkout.scan('A')
      checkout.scan('A')
      total = checkout.total

      expect(total).to eq 130
     end
   end

Notice that some new contexts have been added;

  1. 'when B, A and then D have been scanned'
  2. 'when B, A and then D have been scanned'
  3. 'when A, A, and another A have been scanned'

These new contexts are meant to handle the situations for discounts.

These tests run successfully;

./bin/rspec spec/checkout_spec.rb:4
Run options: include {:locations=>{"./spec/checkout_spec.rb"=>[4]}}

Randomized with seed 1369

checkout
  when B, A and another B has been scanned
    shows the total discounted price of B plus price of A
  when nothing has been scanned
    shows total of zero
  when A has been scanned
    shows the price of A
  when D has been scanned
    shows the price of D
  when B has been scanned
    shows the price of B
  when A, A, and another A have been scanned
    shows the total discounted price of three A items
  when C has been scanned
    shows the price of C
  when B, A and then D have been scanned
    shows the total price of B, A and D

Finished in 0.00125 seconds (files took 0.05214 seconds to load)
8 examples, 0 failures

Randomized with seed 1369


Press ENTER or type command to continue

Conclusion:

This concludes the series.

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

The final project can be found here.

No comments yet. Be the first to comment.