Learning TDD With RSpec Part 3

Photo by Poster POS on Unsplash

Learning TDD With RSpec Part 3

Checkout Kata 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(0end

   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(0end
··
  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.