Practicing Advanced TDD with Ruby through ShoppingCart Enhancements

Introduction to the Shopping Cart Module

Welcome to your fifth and final unit of this course, dedicated to practicing Test-Driven Development (TDD) utilizing Ruby and RSpec. We're going to finish building our ShoppingCart system by adding even more features to our class.

In this course, emphasis is placed on hands-on practice, where you'll receive requirements through tests, one at a time. Your task is to write tests AND implement code that makes each test pass, simulating a real-world TDD environment. Previously, I wrote the tests for you; this time, it's all up to you!

Remember to use the core concepts of the Red-Green-Refactor cycle while completing these coding exercises. I'm still here to help! Just ask.

Final Requirements for `ShoppingCart` Class

In this section, you'll learn how to implement the following features:

  1. Enforcing a Quantity Limit for a Single Item
  2. Retrieving Item Details by ID
  3. Applying and Validating Discount Codes
  4. Handling the Addition of Existing Items and Ensuring Quantity Limits

11. Quantity Limit for a Single Item

  • Description: The cart should enforce a maximum quantity limit of 10 for a single type of item, preventing more than the allowed amount from being added.
  • Details
    • Utilize the add_item(item, quantity) method to add items to the cart.
    • Ensure an exception is raised when adding a quantity that exceeds a limit of 10 for a single item.
    • The exception message should clearly state, "Maximum quantity exceeded" when the limit is breached.
  • Examples: Attempting to add 11 units of Product.new('1', 'Book', 10) should raise an exception indicating "Maximum quantity exceeded."
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    new_quantity = existing_item ? existing_item[:quantity] + quantity : quantity
    raise "Maximum quantity exceeded" if new_quantity > 10

    if existing_item
      existing_item[:quantity] = new_quantity
    else
      @items << item.merge(quantity: quantity)
    end
  end
end

# Example Usage
cart = ShoppingCart.new
begin
  cart.add_item({ id: '1', name: 'Book', price: 10 }, 11)
rescue => e
  puts e.message # Output: "Maximum quantity exceeded"
end

12. Retrieving Item Details by ID

  • Description: When an item is added to the cart, it should be possible to retrieve the item details using its ID, which includes the product information and its quantity in the cart.
  • Details
    • Enable items to be added using an add_item() method with specific IDs.
    • Ensure get_item(id) returns the correct item details, including the quantity after being added to the cart.
  • Examples: Adding an item with Product.new('1', 'Book', 10) and retrieving it by ID 1 should return {'id' => '1', 'name' => 'Book', 'price' => 10, 'quantity' => 1}.
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    if existing_item
      existing_item[:quantity] += quantity
    else
      @items << item.merge(quantity: quantity)
    end
  end

  def get_item(id)
    item = @items.find { |i| i[:id] == id }
    item_details = item&.slice(:id, :name, :price, :quantity)
    item_details
  end
end

# Example Usage
cart = ShoppingCart.new
cart.add_item({ id: '1', name: 'Book', price: 10 }, 1)
puts cart.get_item('1') # Output: {"id"=>"1", "name"=>"Book", "price"=>10, "quantity"=>1}

13. Applying Discount Codes

  • Description: Applying a valid discount code should reduce the total price of items in the cart by the specified discount percentage.
  • Details
    • Use the apply_discount_code(code) method to apply a discount.
    • Support valid discount codes like 'HOLIDAY25' for a 25% discount.
    • Update get_total() to reflect the discounted price.
  • Examples: Applying the discount code 'HOLIDAY25' to an item {'id' => '1', 'name' => 'Book', 'price' => 100} should reduce the total to 75.
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    if existing_item
      existing_item[:quantity] += quantity
    else
      @items << item.merge(quantity: quantity)
    end
  end

  def get_item(id)
    item = @items.find { |i| i[:id] == id }
    item_details = item&.slice(:id, :name, :price, :quantity)
    item_details
  end
end

# Example Usage
cart = ShoppingCart.new
cart.add_item({ id: '1', name: 'Book', price: 10 }, 1)
puts cart.get_item('1') # Output: {"id"=>"1", "name"=>"Book", "price"=>10, "quantity"=>1}

14. Invalid Discount Code

  • Description: The system should not accept discount codes that are invalid and should raise an appropriate exception when such a code is applied.
  • Details
    • Ensure apply_discount_code(code) checks against a list of valid codes.
    • Raise an exception with the message "Invalid discount code" if the code is not valid.
  • Examples: Applying the discount code 'INVALID' after adding an item should raise an exception indicating the code is invalid.
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
    @discount = 0
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    if existing_item
      existing_item[:quantity] += quantity
    else
      @items << item.merge(quantity: quantity)
    end
  end

  def apply_discount_code(code)
    valid_codes = { 'HOLIDAY25' => 0.25 }
    @discount = valid_codes[code] || (raise "Invalid discount code")
  end

  def total
    subtotal = @items.sum { |item| item[:price] * item[:quantity] }
    discount_amount = subtotal * @discount
    subtotal - discount_amount
  end
end

# Example Usage
cart = ShoppingCart.new
cart.add_item({ id: '1', name: 'Book', price: 100 }, 1)
begin
  cart.apply_discount_code('INVALID')
rescue => e
  puts e.message # Output: "Invalid discount code"
end

15. Adding an Existing Item

  • Description: When an item that already exists in the cart is added again, its quantity should increase without duplicates, and the total price should reflect the cumulative price.
  • Details
    • Allow items to be added again using the add_item() method without creating duplicates in the cart.
    • Ensure get_item(id) returns the updated quantity after adding the same item.
    • Update the total price to reflect the price of the added items' cumulative quantities.
  • Examples: Adding Product.new('1', 'Book', 200) twice should result in a quantity of 2 for that item, with the total updated price reflecting the double addition.
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    if existing_item
      existing_item[:quantity] += quantity
    else
      @items << item.merge(quantity: quantity)
    end
  end

  def get_item(id)
    item = @items.find { |i| i[:id] == id }
    item_details = item&.slice(:id, :name, :price, :quantity)
    item_details
  end

  def total
    @items.sum { |item| item[:price] * item[:quantity] }
  end
end

# Example Usage
cart = ShoppingCart.new
cart.add_item({ id: '1', name: 'Book', price: 200 }, 1)
cart.add_item({ id: '1', name: 'Book', price: 200 }, 1)
puts cart.get_item('1') # Output: {"id"=>"1", "name"=>"Book", "price"=>200, "quantity"=>2}
puts cart.total # Output: 400

16. Adding to an Existing Item Respects Maximum Quantity

  • Description: When an existing item in the cart has more units added to it, the total quantity should not exceed the predefined maximum limit.
  • Details
    • Utilize the add_item(item) method to add to an existing item.
    • Ensure that adding a quantity that results in exceeding the maximum allowed quantity raises an exception.
    • The exception message should be "Maximum quantity exceeded" when the quantity limit is breached.
  • Example: Adding 3 units of Product.new('1', 'Book', 200) to an existing 8 units should raise an exception, as it exceeds the limit.
class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item, quantity = 1)
    existing_item = @items.find { |i| i[:id] == item[:id] }
    if existing_item
      new_quantity = existing_item[:quantity] + quantity
      raise "Maximum quantity exceeded" if new_quantity > 10
      existing_item[:quantity] = new_quantity
    else
      raise "Maximum quantity exceeded" if quantity > 10
      @items << item.merge(quantity: quantity)
    end
  end

  def total
    @items.sum { |item| item[:price] * item[:quantity] }
  end
end

# Example Usage
cart = ShoppingCart.new
cart.add_item({ id: '1', name: 'Book', price: 200 }, 8)
begin
  cart.add_item({ id: '1', name: 'Book', price: 200 }, 3)
rescue => e
  puts e.message # Output: "Maximum quantity exceeded"
end

Summary and Preparation for Practice Exercises

In this unit, you reviewed descriptions for more advanced test cases to expand the functionality of the ShoppingCart class, covering features like handling non-existent items, applying discounts, and updating item quantities. With your solidified TDD skills, let's proceed to writing targeted test cases that capture essential functionality for a robust ShoppingCart class.

As you undertake these exercises, remember to engage in the Red-Green-Refactor cycle. Be sure to practice writing tests first, and do not write implementation code unless the test asks for it.

And you're almost done with this practice course! Great work! You're almost there.

Red! Green! Refactor!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal