Want to see the full-length video right now for free?Sign In with GitHub for Free Access
Let's get into the nuts and bolts of TDD.
In this example, we're going to build a calculator through a test-driven process. For our Ruby tests, we're going to use RSpec, a popular testing library with an expressive DSL and excellent community support. We'll use RSpec's autorun feature during this example to run tests every time we update our files. We don't typically use this feature, but it will make checking our work during the Trail faster.
We'll start by writing a test. RSpec uses nested blocks to group tests together.
The outermost block describes the class (
Calculator), and the next level in
describes our method,
#add. One more level in, and we write the spec for our
first behavior inside an
describe Calculator do describe "#add" do it "adds two numbers" do calculator = Calculator.new expect(calculator.add(1, 1)).to eq(2) end end end
The contents of the spec are mostly the Ruby you already know. RSpec's DSL
provides some methods that give your tests some English readability.
is how you tell RSpec to perform some verification of test results.
The first run of the test suite gives us an error. This is good! Errors in TDD help us determine what incremental step to take next. The new code should implement the simplest possible logic (within reason) to resolve the error.
First, we add the class, as our error is due to
Calculator not being defined.
Adding the class will then lead to an error about an undefined method
Calculator#add will then lead to an error about an incorrect
number of arguments. Giving
#add arguments will then lead to an error about
the returned value being
nil rather than the sum. Now it's time to implement
our method logic.
class Calculator def add(a, b) end end
#add to return
15 and make the test pass may seem like a bad
idea, but we're not done yet. Now that our test passes, we'll write another test
that requires a more generalized solution. A second test exposes our hard-coded
return value as an inadequate solution and requires a better implementation to
make both tests pass.
It's not always necessary to resolve tests initially through a hard-coded value, but the initial implementation and the quick iteration to a general implementation illustrates how TDD can guide you to the minimum code needed to deliver the feature.
#add method working as we want, it's time to add a new capability to
our calculator, to return the result of a factorial.
Factorial has two cases: factorial for a number is all of the positive integers
from one up to that number multiplied by each other. Factorial
0, the special
1. We'll write a test to handle each of these cases:
describe "#factorial" do it "returns 1 when given 0 (0! = 1)" do calc = calculator.new expect(calc.factorial(0)).to eq(1) end it "returns 120 when given 5 (5! = 120)" do calc = calculator.new expect(calc.factorial(5)).to eq(120) end end
We'll resolve errors for each of the tests individually, starting with the
simplest case, factorial
0. First, we define the method, then we give the
method an argument, then we return the expected result.
def factorial(n) 1 end
Our spec passes, and we can move on to the more complicated general case, which
is still failing. A recursive solution is straightforward approach, with an
clause guarding against the
def factorial(n) if n == 0 1 else n * factorial(n-1) end end
With passing tests, we're green across the board. This state is a great time to do some refactoring, while your understanding of the code is fresh and your test suite is ready to backstop you against regressions.
Now it's time to get your hands dirty with a TDD exercise.
gem install rspec
# Use TDD principles to build out name functionality for a Person. # Here are the requirements: # - Add a method to return the full name as a string. A full name includes # first, middle, and last name. If the middle name is missing, there shouldn't # have extra spaces. # - Add a method to return a full name with a middle initial. If the middle name # is missing, there shouldn't be extra spaces or a period. # - Add a method to return all initials. If the middle name is missing, the # initials should only have two characters. # # We've already sketched out the spec descriptions for the #full_name. Try # building the specs for that method, watch them fail, then write the code to # make them pass. Then move on to the other two methods, but this time you'll # create the descriptions to match the requirements above. class Person def initialize(first_name:, middle_name: nil, last_name:) @first_name = first_name @middle_name = middle_name @last_name = last_name end # implement your behavior here end RSpec.describe Person do describe "#full_name" do it "concatenates first name, middle name, and last name with spaces" it "does not add extra spaces if middle name is missing" end describe "#full_name_with_middle_initial" describe "#initials" end