Intro to TDD
The Ruby community is healthily obsessed with testing. Let’s look at how to get started with Test Driven Development (TDD).
Get Ready
Prerequisites
Before starting this tutorial, you should have a basic understanding of topics covered in Ruby in 100 Minutes, including:
- classes
- objects
- methods and arguments
- string interpolation
Learning Goals
After completing this tutorial, you should be able to:
- install a Ruby gem
- create and run a minitest test suite
- write assertions using minitest
- read error messages
- fix error messages
- read test failures
- fix failing assertions
- explain and demonstrate the TDD workflow
What We’re Doing in This Tutorial
We’ll be implementing a small unicorn class using TDD. Here are the basic requirements:
- unicorns have names
- they are usually white (but can be any color)
- they say "sparkly" things (don’t worry, we’ll explain what this means!)
Setup
There are several libraries that can help us write tests, and we’re going to use one of the simplest ones: minitest.
Minitest comes with Ruby by default, but we’re going to go get the latest version by installing the gem manually:
Terminal
$
|
|
We need a place to put our code while working. Create an empty directory, and change into it:
Terminal
$ $ |
|
Experimenting with minitest
Create an empty file called unicorn_test.rb
.
Terminal
$
|
|
Open this file in your text editor.
Configuring Minitest
Minitest is included with Ruby, but we want to use the gem version we installed
instead of the one that’s built in. Stick this at the top of the
unicorn_test.rb
file:
1
|
|
Now that we’ve told our code where to find minitest, we need to tell it to actually load it. Add this line below what you have:
1
|
|
The next line is optional. It adds pretty colors in your terminal when you run your tests. TDD is a little more fun in color!
1
|
|
Starting The Test Suite
A test suite is a collection of tests. Each test in the collection is an example of using the code we write, and proves that the code works for that particular example.
Still in unicorn_test.rb
, we create a new test suite by defining a class that
will inherit from Minitest::Test
:
1 2 |
|
To add a test inside the class, we add a method whose name starts with test
:
1 2 3 4 |
|
Running the Suite
Save the file and switch back to your terminal. Make sure that you’re in the
same directory as the unicorn_test.rb
file. Run your test suite by
executing the following command in your terminal.
Terminal
$
|
|
Terminal
|
When you run the test, you’ll get a dot for each passing test. Let’s add more tests:
1 2 3 4 5 6 7 8 9 10 |
|
Run the test suite again the same way we did above. Your output should look like this:
Terminal
|
Since our test suite has three tests, we see three dots.
Test Assertions
If you look at the bottom line, though, it says that it has 3 runs, but 0 assertions.
The dictionary defines assertion as a confident and forceful statement of fact or belief.
Our test suite is passing, but it’s not making any confident and forceful statements of fact or belief. In other words, the only reason it’s currently passing is because it isn’t failing. And the only reason it’s not failing, is because it’s not even trying anything.
Adding Assertions
Let’s give the test suite some work to do. In unicorn_test.rb
, add the
assert_equal
line to your first test.
1 2 3 4 5 6 7 8 9 10 11 |
|
assert_equal
is a method that comes with minitest. It takes two parameters:
* the expected value (3)
* the actual value (1+1)
By saying assert_equal 3, 1 + 1
, we are saying that we expect 3 to be equal to
1+1.
Run the test again:
Terminal
$
|
|
Unsurprisingly, the output tells you that the test is failing:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Two dots, one F
.
Try running your test suite a few more times and watch the order of the dots and the F change! Minitest randomizes the order that your tests get run in. This ensures that each test runs independently of the others.
Reading the Failure
In addition to telling you that a test is failing, the output also tells us quite a bit about the failure:
1
|
|
1 2 3 |
|
1 2 3 4 5 6 7 |
|
Take a look at the summary. We’ve got three runs (a.k.a. tests), one assertion, and zero failures.
1
|
|
We also have zero errors and zero skips.
Errors and Failures are Not the Same Thing
A failure means that we expected one value but got another. An error means that something was wrong with the code and couldn’t even run. Let’s introduce an error:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
If you run that, you’ll get:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
2 dots, one E
. The summary says we have one error, and the output tells us a
bit about it:
1
|
|
It’s happening in the UnicornTest
test suite, in a test named
test_something_else
.
1
|
|
It’s a NoMethodError
, and it’s saying that we can’t subtract from nil
,
because nil
doesn’t have a minus method.
Then it tells us that it’s blowing up on line 10 of the test suite:
1
|
|
Skipping a Test
Now add skip
to the top of the test with the error in it:
1 2 3 4 |
|
Run the test again:
1 2 3 4 5 6 7 8 9 |
|
Skipping a test means that it won’t be reported as an error or failure.
This can be helpful if you have a lot of failures and want to focus on one at
a time. Just skip the ones you want to ignore for a while, and then delete
the skip
in one test at a time.
Back to Passing
Now get the test passing by replacing nil
with 2
:
1 2 3 |
|
Unicorn Time!
OK, we’re ready to start our Unicorn. Delete all the something tests from unicorn_test.rb
Your unicorn_test.rb
should currently look like this:
1 2 3 4 5 6 |
|
Remember that a Unicorn:
- has a name.
- has a color, though it’s normally white
- says sparkly things.
Our First Test: A Unicorn Has a Name
Now, we’ll create the first, empty test for the first requirement: Unicorns have names.
1 2 3 4 |
|
And now, we’re ready to make a bold statement about how unicorns are named.
Writing the Assertion
We haven’t written any code that implements unicorns, so we’re going to do some wishful thinking. When we make a new unicorn, we will want to create it with a name like this:
1
|
|
Let’s add some code to the test with that behavior:
1 2 3 |
|
This test isn’t asserting anything. It’s just creating a new unicorn.
We need to make an assertion:
1 2 3 4 |
|
So what is the expected_value
here? Well, we called the unicorn "Robert", so that
would be our expected value.
1 2 3 4 |
|
Now, what is actual_value
? How do we ask a unicorn about its name? Probably like this:
1
|
|
So in the test:
1 2 3 4 |
|
But What is unicorn
?
It’s looking better, but this isn’t going to work because we never defined
unicorn
. We have to save our new unicorn to a local variable:
1 2 3 4 |
|
Run the Test
Finally, we have made a forceful statement of fact about how unicorns are named. With all of this in place, the test suite now looks like this:
1 2 3 4 5 6 7 8 9 10 |
|
Run the tests:
Terminal
$
|
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The lonely E
means error
, and the actual error that we received is a
NameError
:
1
|
|
Reading the Error
Our test suite is trying to create a new unicorn (Unicorn.new
), but it doesn’t
know about any class called Unicorn
.
We’re going to create a Unicorn
and put it in its own file. Let’s pretend we
have already created that file. Below the minitest/pride
line let’s require
this pretend file using the following line of code.
1
|
|
require_relative
means look for a file named unicorn.rb
in the directory
path relative to the current file that we’re in. So here we’ll look for a
file named unicorn.rb
in the same directory as unicorn_test.rb
.
The test now looks like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
Did the Unicorn Magically Appear?
Let’s run our test suite. It blows up!
1 2 |
|
The error we’re getting is LoadError
, which is Ruby’s way of saying that
it’s trying to load a file (unicorn.rb
) but can’t find it.
That’s because we haven’t created it yet.
Making a unicorn.rb
If we create an empty file, we’ll fix the LoadError
. From terminal:
1
|
|
Now we are back to the same error as before (NameError
):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Defining the Unicorn
Class
We can fix the name error my creating an empty class inside the new
unicorn.rb
file:
1 2 |
|
A New Error!
Run the tests again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
An argument error means that we’re calling a method incorrectly. Read the error
closely. We see that the problem is in the initialize
method that gets
called when we call new
.
What does the wrong number of arguments(1 for 0)
mean? Our tests are calling
initialize
with one argument, but it doesn’t accept any arguments.
In other words, we’re saying Unicorn.new("Robert")
, but right now, initialize
is defined without accepting arguments like this:
1 2 |
|
We need to make it so our initialize
method accepts the argument:
1 2 3 4 |
|
What’s in a .name
?
Run the tests.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Read the error:
1
|
|
A NoMethodError
means that when we said unicorn.name
, the unicorn tried to
find a method named name
to run but it didn’t exist.
Adding a name
method
We know that the Unicorn doesn’t have a method called name
. That’s easy enough to fix, we’ll
create one:
1 2 3 4 5 6 7 |
|
Run the tests again.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 |
|
This won’t work for all unicorns, but it should be good enough to get the test passing:
1 2 3 4 5 6 7 |
|
Maybe All Unicorns Are Named "Robert"
Fine, the test is passing, but we have a problem. All unicorns are now named Robert.
Try it in IRB:
IRB
|
|
The ./
part means "Look in the current directory for a file named
unicorn.rb
".
IRB
|
Our test suite needs to be smarter. It needs to force us into implementing a more robust naming mechanism.
Triangulation Tests
We’ll create a second test to triangulate the behavior that we want.
1 2 3 4 5 6 7 8 9 10 11 |
|
As expected, it fails.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We are passing the name in when we call Unicorn.new, so our initialize method is receiving it, but we’re not saving it anywhere. Let’s make a way to save it from the argument into the object.
Storing the Passed-In Name
Let’s tell it to take the name
argument that was passed in and save it to
@name
. Then, in the name
method, we will return that instance variable.
1 2 3 4 5 6 7 8 9 |
|
That gets the test passing:
1 2 3 4 5 6 7 |
|
Our tests have guided us towards an implementation.
Unicorns Are White
The next requirement says that unicorns are usually white, but can be any color. That sounds like more than one requirement, so let’s focus on the first part.
Testing Color
Let’s see that a Unicorn is white when we haven’t specified any other color:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Unicorns Have No Color?
Running the test gives us an error:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A NoMethodError
. We don’t have a method called color
.
Splash of color
That’s easy to fix, we’ll just add a method color
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
See the Output Change
It doesn’t do anything yet, but it should be enough to change the output from the test, and the new output from the test should tell us what to do next:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This is a failed expectation, meaning that our logic in the color
method is
wrong. That makes sense, the color
method is empty.
A Dummy Response
To get the test passing, we can hard-code "white" as the response:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
That gets the test passing.
1 2 3 4 5 6 7 |
|
A white?
Method
In addition to asking the Unicorn what its color is, let’s also make it so we can ask it if it is white:
1 2 |
|
In Ruby, a method can end with a question-mark, which typically is used as a signal that this the method returns true or false.
The test looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
So the test will pass if white?
returns true, and it will fail if it returns
false.
assert_equal
vs assert
This is such a common test case that minitest has a special method to assert
that the response should be true
. Let’s use assert
instead of assert_equal
.
1 2 3 4 |
|
The test is going to blow up, of course, because we don’t have code to support the feature yet:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Implementing white?
As usual, we can fix a NoMethodError
by defining the empty method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Running the test again gives us new output. A failure this time:
1 2 3 4 5 6 7 8 9 10 11 |
|
When we were using assert_equal
we got output that said:
1 2 |
|
Instead, it says:
1
|
|
Defining a Message for assert
The Failed assertion
bit seems fairly straight-forward.
When we use assert
rather than assert_equal
the error message is
different. We know what the expected value is (true), and if it’s not true,
then presumably it’s false
.
What about the no message given
bit, though? What message is that?
Let’s tweak the test a bit. We’ll add a message after our assertion to help the us know what went wrong. It has nothing to do with production code, it’s just some helpful context.
1 2 3 4 |
|
Run the test again, and you’ll see the helpful error message we added.
1 2 3 4 5 6 7 8 9 10 11 |
|
Is nil
the same as false
?
OK, so back to the error message. The test will pass if white?
returns true,
but the test is failing. What is white?
returning? What does the method look
like?
1 2 |
|
Wait, that’s not returning false
, it’s returning nil
.
In Ruby we talk about something being truthy or falsy, and there are
exactly two objects that are falsy: nil
, and false
. All other objects are
truthy. The string "hello". The number 39. An instance of unicorn
. They’re
all truthy.
So when we say assert unicorn.white?
then as long as white?
returns a
truthy value, the test will pass.
It’s currently returning nil
, which is falsy, so the test is failing.
Always True
Make it pass by hard-coding true
as the return value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Run the tests and they should pass.
1 2 3 4 5 6 7 |
|
Purple Unicorns
OK, so we’ve got the name thing working. Unicorns are white by default (and they know it). Let’s make a purple unicorn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
This blows up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The error is an ArgumentError
, which means that we’re calling a method
wrong. Which method? initialize
. Who calls initialize
? new
does.
The error says 2 for 1
meaning that we’re giving it two arguments: name AND
color, but it only takes one: name.
Multiple Arguments to initialize
Again, this is what we gave it:
1
|
|
What it expects:
1 2 3 |
|
Change the code so that the initialize
method accepts two arguments, one for
name and one for color:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
See the Results
Run the tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
What just happened? Look at the summaries:
1
|
|
1
|
|
Not a single test is passing. Let’s look at all those errors:
1 2 3 4 5 6 7 8 9 10 11 |
|
It’s the same error four times.
It says we’re sending one argument, but remember: we changed the method so that it takes two.
1 2 3 |
|
Let’s look at our tests:
1 2 3 4 5 6 7 8 9 10 11 |
|
We’re in a bit of a bind. Most of the tests need this:
1 2 3 |
|
But one of the tests needs this:
1 2 3 |
|
We could change all the other tests to take "white", but that would defeat the purpose. The unicorn is white by default. That means we shouldn’t have to specify it.
A Default Argument
Ruby gives us a way to define a default value for an argument with the syntax
argument=defaultvalue
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Run the test, and all the tests should be back to passing. We still have a single failure, and that’s the test we’re interested in:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Purple != White
We want Barbara to be white, but she’s purple.
The color gets passed in to initialize, so let’s save it, the same way that we do with name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Now, run the tests, and we get a single failure:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Which test? It’s not the Barbara test, it’s Margaret. She expects to be white,
but she’s "default color". That’s because she’s getting the default value in
initialize
.
Defaulting to White
Change the default to be "white":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
This gets all the tests passing.
1 2 3 4 5 6 7 |
|
Returning to white?
We aren’t quite done with the color requirement. The white?
method is still
always going to return true
. Add a test for a unicorn that is not white:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Using refute
There’s a short-hand for assert_equal false, something
, and that’s refute
.
According to the dictionary, to ‘refute’ something is to deny or contradict
it. refute
is the opposite of assert
. Where a test with assert
will pass
if the return value is true
, a test with refute
will pass if the return
value is false.
1 2 3 4 |
|
Run the tests, and the last one will fail:
1 2 3 4 5 6 7 8 9 10 11 |
|
Adding a Message
If we had been using assert_equal
, the error message would have been:
1 2 |
|
Now we have a message that says:
1
|
|
It’s expecting white?
to return false, but the return value is hard-coded:
1 2 3 |
|
Clearly that’s not going to work.
As with assert
, we can also give it a message:
1 2 3 4 |
|
Run it:
1 2 3 4 5 6 7 8 9 10 11 |
|
The Failed refutation, no message given
message has been
replaced with the one we just wrote.
Real Implementation of white?
We’re not going to get away with hard-coding an answer anymore. We’ll solve it by comparing the actual color to the string "white":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This gets the test passing:
1 2 3 4 5 6 7 |
|
We’ve now gotten the first and second requirements taken care of. Unicorns have names, and they’re usually white, although they may be a different color.
Unicorns Can Talk
The last requirement has to do with how unicorns speak. They say sparkly things.
It’s not entirely clear from the requirements what that means. It kind
of sounds like unicorns are very optimistic, and only say positive ("sparkly")
things. We could create a speak
method that chooses a random response from a
list of positive-sounding things, but that’s going to be kind of hard to test,
and besides, it’s boring if the unicorn is restricted to only saying a handful
of things.
Talking with Sparkles
Instead, let’s let the unicorn say anything, but make sure that whatever it is they say it with ASCII sparkles:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Running the Tests
The test blows up:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We’re missing the method say
on unicorn.
Defining .say
That’s easy enough to fix:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Adding the Parameter
The test still isn’t happy, though:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
We’re calling say
wrong. Wrong number of arguments, 1 for 0
. That’s
familiar. In the test we’re calling say
with an argument:
1
|
|
And here’s how the say
method is defined:
1 2 |
|
The test is right, the code is wrong. Let’s make the say
method accept an
argument:
1 2 |
|
This changes the message of the test, but we’re not passing yet:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Returning Sparkles
No surprise there. The method is returning nil. Copy/paste from the error message in the test into the method definition:
1 2 3 |
|
Run the tests:
1 2 3 4 5 6 7 |
|
The test passes, but the implementation is kind of dumb.
Let’s Go Deeper
Let’s make a test that forces us to write better code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Now we get a failing test, thankfully:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Using String Interpolation
We can make it pass by interpolating the argument into the string with the ASCII sparkles:
1 2 3 |
|
This gets the final test passing.
1 2 3 4 5 6 7 |
|
Final Results
That’s it. We have fulfilled the original requirements.
This is the code we ended up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
And here is the full test suite that we wrote:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|