Logo

dev-resources.site

for different kinds of informations.

Test-Driven Development (TDD) in Ruby: A Step-by-Step Guide

Published at
11/1/2024
Categories
tutorial
bestpractices
ruby
learning
Author
jetthoughts-dev
Author
15 person written this
jetthoughts-dev
open
Test-Driven Development (TDD) in Ruby: A Step-by-Step Guide

In Test-Driven Development (TDD), you start with tests, not code. First, write a test that defines what the code should do. Then write the code to make the test pass, focusing on small, clear steps. This approach in Ruby helps keep each part of the code clear and on target with what it’s supposed to do, often making it easier to adjust and maintain down the line. TDD can make a difference in breaking down problems into smaller, manageable steps, which tends to reduce bugs and make sure each part does what’s actually needed.

In this article, we’ll explore the TDD lifecycle—red, green, refactor—through a small example of an Order class that handles items and calculates the total price.


The TDD Lifecycle

The TDD lifecycle is often broken into three steps:

  • Red: Write a test that defines a new function or behavior. This test will fail initially.
  • Green: Implement just enough code to make the test pass.
  • Refactor: Clean up the code, improving structure without changing functionality.

This cycle ensures that code only has the functionality required by the tests, keeping it lean and easy to refactor.


Step-by-Step Example: Building an Order Class with TDD

Imagine we’re building an e-commerce app. One part of this app is an Order class that will:

  1. Allow items to be added with prices and quantities.
  2. Calculate the total price for all items in the order.
  3. Apply a discount if specified.

Step 1: Define the Initial Test

To start, we’ll write a test specifying that a new order has a total price of zero when no items are added.

require 'minitest/autorun'
require_relative 'order'

class OrderTest < Minitest::Test
  def test_initial_total_price_is_zero
    order = Order.new
    assert_equal 0, order.total_price
  end
end
Enter fullscreen mode Exit fullscreen mode

This test defines our expectations: a new order should start with a zero total. Since there’s no Order class or total_price method yet, running this will result in a “red” failure.

Step 2: Implement Just Enough Code to Pass

Next, we implement the bare minimum to make this test pass.

class Order
  def total_price
    0
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, running the test again should turn it "green", confirming that our initial requirement is met.

Step 3: Expanding Requirements - Adding Items

Now let’s add a requirement to add items to the order. Our new test will check that the total price updates based on items added.

def test_add_item_increases_total_price
  order = Order.new
  order.add_item(name: "Book", price: 10, quantity: 2)
  assert_equal 20, order.total_price
end
Enter fullscreen mode Exit fullscreen mode

Implementing add_item
To make this test pass, we need to implement add_item in the Order class. We’ll use an array to store items, and total_price will sum them.

class Order
  def initialize
    @items = []
  end

  def add_item(name:, price:, quantity:)
    @items << { name: name, price: price, quantity: quantity }
  end

  def total_price
    @items.reduce(0) { |sum, item| sum + item[:price] * item[:quantity] }
  end
end
Enter fullscreen mode Exit fullscreen mode

Now our test_add_item_increases_total_price test should pass.

Step 4: Adding Validation through Tests

To make this class robust, we’ll add validation to ensure item prices and quantities are positive. Let’s define two tests:

def test_add_item_with_negative_price_raises_error
  order = Order.new
  assert_raises(StandardError) { order.add_item(name: "Book", price: -5, quantity: 1) }
end

def test_add_item_with_negative_quantity_raises_error
  order = Order.new
  assert_raises(StandardError) { order.add_item(name: "Book", price: 5, quantity: -1) }
end
Enter fullscreen mode Exit fullscreen mode

Implementing Validation in add_item
To make these tests pass, we’ll add basic validation in add_item.

def add_item(name:, price:, quantity:)
  raise "Price must be positive" if price <= 0
  raise "Quantity must be positive" if quantity <= 0
  @items << { name: name, price: price, quantity: quantity }
end
Enter fullscreen mode Exit fullscreen mode

With this validation in place, our new tests should pass, and the code becomes safer to use.

Step 5: Refactoring

As more features and validations are added, the add_item and total_price methods can grow in complexity. Refactoring keeps our codebase clean and maintainable without affecting functionality. Since we have tests, we’re confident that any changes we make will preserve the behavior.

Let’s refactor total_price to separate the logic for calculating an item’s total cost. We can move it into a private helper method, which simplifies the main method.

class Order
  def total_price
    @items.reduce(0) { |sum, item| sum + item_cost(item) }
  end

  private

  def item_cost(item)
    item[:price] * item[:quantity]
  end
end
Enter fullscreen mode Exit fullscreen mode

Our tests for total_price will confirm that this refactoring didn’t break anything. This is a key benefit of TDD: the tests give us freedom to refactor while ensuring functionality remains intact.

Benefits of TDD

Using TDD in Ruby offers several advantages:

  • Requirement Clarity: Writing tests first clarifies requirements early on.
  • Confidence in Code: Tests act as a safety net, catching regressions and errors introduced by refactoring or new features.
  • Incremental Development: By breaking down requirements into small, testable increments, TDD encourages focused, incremental development.

References:

Kent Beck's Test-Driven Development: By Example offers a comprehensive step-by-step guide to mastering Test-Driven Development (TDD). It demonstrates how testing can lead to clean and effective code design. Sandi Metz's 99 Bottles of OOP complements this approach by providing practical exercises that help developers create flexible and maintainable object-oriented code using a test-driven mindset.

  • Beck, Kent. Test Driven Development: By Example. Addison-Wesley Professional, 2002. ISBN: 0321146530.

  • Metz, Sandi and Owen, Katrina. 99 Bottles of OOP: A Practical Guide to Writing Cost-Effective, Maintainable, and Pleasing Object-Oriented Code. Self-published, 2018. ISBN-13:978-1-944823-00-9

bestpractices Article's
30 articles in total
Favicon
Why Test Driven Development
Favicon
Go Serialization Essentials: Struct Tags, Error Handling, and Real-World Use Cases
Favicon
Creating Safe Custom Types with Validation in Go
Favicon
Best Practices for Network Management in Docker 👩‍💻
Favicon
Enforce DevOps best practices and eliminate production errors!
Favicon
The State of Cybersecurity Marketing: A Deep Dive Analysis
Favicon
Responsive Images: Best Practices in 2025
Favicon
Code Speaks for Itself: Métodos Bem Escritos Dispensam Comentários
Favicon
Generate 6 or 8 digit alpha numeric code using best performance in C#
Favicon
How To Replace Exceptions with Result Pattern in .NET
Favicon
8 essentials for every JavaScript project
Favicon
Send a From Header When You Crawl
Favicon
The Open Source AI : Understanding the New Standard
Favicon
Best Practices for Using Azure ATP in Hybrid Environments
Favicon
TADOConnection: Proper Use of LoginPrompt
Favicon
Best Practices for Developing and Integrating REST APIs into Web Applications
Favicon
Mastering Cybersecurity: A Comprehensive Guide to Self-Learning
Favicon
Best Practices for Data Security in Big Data Projects
Favicon
Hopefully Helpful Notes, Best Practices, ...
Favicon
Best Practices for Using GROUP BY in MySQL for Converting Vertical Data to JSON
Favicon
Best Practices in Software Architecture for Scalable, Secure, and Maintainable Systems
Favicon
Cloud Computing Security Best Practices for Enterprises
Favicon
Why Interfaces Are Essential in .NET Development
Favicon
Git Tricks You Should Know: Aliases, Bisect, and Hooks for Better Workflow
Favicon
Why pinning your dependency versions matters
Favicon
Microservices Best Practices: Multi-Tenant microservices with Java SDK
Favicon
Apple Intelligence: Pioneering AI Privacy in the Tech Industry
Favicon
Improving JavaScript Performance: Techniques and Best Practices
Favicon
Test-Driven Development (TDD) in Ruby: A Step-by-Step Guide
Favicon
APIs and Security Best Practices: JavaScript and Python Examples

Featured ones: