Lesson 3
Introduction to TDD with Ruby and RSpec: Building a `calculate_total` Method
Introduction and Overview

By now, you should be familiar with the basics of Test-Driven Development (TDD) and its iterative Red-Green-Refactor cycle. This lesson focuses on honing your TDD mindset, a perspective that prioritizes writing tests before coding, which can dramatically improve code clarity, reliability, and maintainability.

We'll continue using Ruby and RSpec, tools that streamline our test-driven approach in Ruby projects. RSpec is celebrated for its readability and the expressive nature of its syntax, making it an excellent choice for implementing TDD principles in our projects.

Let's explore this mindset further with practical examples and visualize the flow of thinking in tests.

Example: `calculate_total` Method (Red Phase)

Let's begin by writing tests for a method named calculate_total, designed to compute the total price of items in a shopping cart. This is where you engage with the Red phase: Write a failing test.

Let's think about how to build a method that calculates lists of items. What should the interface be? How does the consumer of the code use it? These are the questions we think about first when we "think in tests." Here's one way we might think about it.

Ruby
1require './cart' 2 3RSpec.describe '#calculate_total' do 4 it 'returns 0 for an empty cart' do 5 expect(calculate_total([])).to eq(0) 6 end 7end

Explanation:

  • We know we want a method called calculate_total(), so we'll write a test that uses it.
  • For now, we know that we want an empty array as input to return 0, so we can write that test.
  • The expectation is that these tests will initially fail, which is an integral part of the Red phase.

Running these tests confirms they fail, creating a clear path for subsequent development.

Example: Passing the Tests (Green Phase)

Now, let’s move to the Green phase, where we implement the minimal code to pass these tests.

Implement the calculate_total method in cart.rb:

Ruby
1def calculate_total(items) 2 0 3end

Explanation:

  • The calculate_total method takes an array of items. We don't really know the shape of the data yet, and that's OK!
  • Returning 0 is enough to get the test to pass.

By running the test suite again, we should see all tests passing, demonstrating that our method meets the required condition.

Example: Refactoring the Code

In the Refactor phase, we fine-tune the code for clarity and performance. It's always OK to do nothing in this step, and that's what we'll do. Let's add another test.

Example: Write Another Test (Red)

Now is the time to think about what kind of data we want to pass to our calculate_total method. We consider that we'd like to pass the name, price, and quantity as a hash in the array. The total will be the product of price and quantity. Let's do that and see how it feels:

Ruby
1RSpec.describe '#calculate_total' do 2 it 'returns 0 for an empty cart' do 3 expect(calculate_total([])).to eq(0) 4 end 5 6 # New test 7 it 'returns correct total for a single item' do 8 expect(calculate_total([{ name: 'Apple', price: 0.5, quantity: 3 }])).to eq(1.5) 9 end 10end

That feels pretty good; the interface seems clear. Let's see if we can get those tests passing.

Example: Make It Pass Again (Green)

Now, we need to think about how to make these tests pass. What is the minimum necessary to get this to pass?

Ruby
1def calculate_total(items) 2 if items.any? 3 items[0][:price] * items[0][:quantity] 4 else 5 0 6 end 7end

When we run the tests, we should see that both tests pass! We're Green!

Example: Refactor

Now we ask ourselves: is there anything we can do to make this code better? One thing I might do is get rid of the repetition of items[0] in the calculate_total method.

Ruby
1def calculate_total(items) 2 if items.any? 3 item = items.first 4 item[:price] * item[:quantity] 5 else 6 0 7 end 8end

When we run the tests again, we're still green. The code is more expressive now and has a better structure, so let's do the Red-Green-Refactor loop again!

Example: Write a Failing Test (Red)

The existing code works! But it won't be very useful to only use the first item. If we add one more test, we can generalize more.

Ruby
1RSpec.describe '#calculate_total' do 2 it 'returns 0 for an empty cart' do 3 expect(calculate_total([])).to eq(0) 4 end 5 6 it 'returns correct total for a single item' do 7 expect(calculate_total([{ name: 'Apple', price: 0.5, quantity: 3 }])).to eq(1.5) 8 end 9 10 it 'fails deliberately with multiple items' do 11 items = [ 12 { name: 'Apple', price: 0.5, quantity: 3 }, 13 { name: 'Banana', price: 0.3, quantity: 2 } 14 ] 15 expect(calculate_total(items)).to eq(2.1) 16 end 17end

As expected, this new test will fail, and we can move to the Green step.

Example: Make the Test Pass (Green)

Let's take a stab at getting the test to pass:

Ruby
1def calculate_total(items) 2 total = 0 3 items.each do |item| 4 total += item[:price] * item[:quantity] 5 end 6 total 7end

This does the job, and we're Green again. I can't help but feel like we could have written that code a bit better. Now that we have tests that cover everything we want this method to do, let's move to the Refactor step!

Example: Refactor

When we look at the calculate_total method, it is clear that this is an "aggregate function." It takes an array of items and reduces it to an aggregate value. Times like this call for more Ruby-centric solutions!

Ruby
1def calculate_total(items) 2 items.sum { |item| item[:price] * item[:quantity] } 3end

The sum method here achieves the goal of aggregation succinctly. I like this code a lot better! Best of all, our tests tell us that we're still Green! We've successfully refactored the code.

Running the Tests

To run your tests with RSpec, ensure you have your working directory set up correctly. Use the following command in the terminal:

Bash
1rspec cart_spec.rb

Make sure you have RSpec installed and your test files are properly configured. Running this command should execute all your tests, showing output similar to:

Bash
1... 2 3Finished in 0.01 seconds (files took 0.088 seconds to load) 43 examples, 0 failures
Summary and Preparation for Practice

Throughout this lesson, we delved into adopting a TDD mindset with a focus on writing tests before coding and following the Red-Green-Refactor cycle meticulously. Here are your key takeaways:

  • Red: Start by writing a failing test. This ensures you have a clear goal and define the interface upfront.
  • Green: Develop just enough code to pass the test, ensuring you fulfill the requirements.
  • Refactor: Optimize your code for readability and maintainability, all while ensuring tests continue to pass.

These principles set a solid foundation for the upcoming practice exercises, further cementing this mindset into your development workflow. Continue practicing the TDD cycle to foster robustness, code clarity, and reliability in your projects. Keep going, and embrace TDD as an essential part of your coding journey.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.