Jumpstart Lab Curriculum

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

$
gem install minitest

We need a place to put our code while working. Create an empty directory, and change into it:

Terminal

$
$
mkdir unicorncd unicorn

Experimenting with minitest

Create an empty file called unicorn_test.rb.

Terminal

$
touch unicorn_test.rb

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
gem 'minitest', '~> 5.0'

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
require 'minitest/autorun'

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
require 'minitest/pride'

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
class UnicornTest < Minitest::Test
end

To add a test inside the class, we add a method whose name starts with test:

1
2
3
4
class UnicornTest < Minitest::Test
  def test_something
  end
end

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

$
ruby unicorn_test.rb

Terminal

 
 
 
 
 
 
 
# Running:.Fabulous run in 0.003111s, 321.4401 runs/s, 0.0000 assertions/s.1 runs, 0 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_something
  end

  def test_something_else
  end

  def test_yet_another_thing
  end
end

Run the test suite again the same way we did above. Your output should look like this:

Terminal

 
 
 
 
 
 
 
# Running:...Fabulous run in 0.001829s, 1640.2406 runs/s, 0.0000 assertions/s.3 runs, 0 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_something
    assert_equal 3, 1 + 1
  end

  def test_something_else
  end

  def test_yet_another_thing
  end
end

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

$
ruby unicorn_test.rb

Unsurprisingly, the output tells you that the test is failing:

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

.F.

Fabulous run in 0.035829s, 83.7311 runs/s, 27.9104 assertions/s.

  1) Failure:
UnicornTest#test_something [unicorn_test.rb:6]:
Expected: 3
  Actual: 2

3 runs, 1 assertions, 1 failures, 0 errors, 0 skips

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
UnicornTest#test_something [unicorn_test.rb:6]:
Look closely at this line. It gives us four important pieces of information: * The test suite that contains the failure: `UnicornTest`. Since we only have one test suite right now, that’s going to be the same each time we run our test suite. * The name of the test that’s failing: `test_something`. Since we only have one test with assertions, this isn’t very surprising either. * The name of the file that contains the failing test: `unicorn_test.rb`. This is extremely helpful when you have a project with hundreds of files with tests in it. * The exact line of code that is actually blowing up: `:6`. ### A Passing Test Let’s make the test pass by changing our expected value.
1
2
3
def test_something
  assert_equal 2, 1 + 1
end
1
2
3
4
5
6
7
# Running:

...

Fabulous run in 0.001997s, 500.7511 runs/s, 500.7511 assertions/s.

3 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Take a look at the summary. We’ve got three runs (a.k.a. tests), one assertion, and zero failures.

1
3 runs, 1 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_something
    assert_equal 2, 1 + 1
  end

  def test_something_else
    assert_equal 1, nil - 1
  end

  def test_yet_another_thing
  end
end

If you run that, you’ll get:

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

.E.

Finished in 0.001890s, 1587.3016 runs/s, 529.1005 assertions/s.

  1) Error:
UnicornTest#test_something_else:
NoMethodError: undefined method `-' for nil:NilClass
    unicorn_test.rb:10:in `test_something_else'

3 runs, 1 assertions, 0 failures, 1 errors, 0 skips

2 dots, one E. The summary says we have one error, and the output tells us a bit about it:

1
UnicornTest#test_something_else:

It’s happening in the UnicornTest test suite, in a test named test_something_else.

1
NoMethodError: undefined method `-' for nil:NilClass

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
unicorn_test.rb:10

Skipping a Test

Now add skip to the top of the test with the error in it:

1
2
3
4
def test_something_else
  skip
  assert_equal 1, nil - 1
end

Run the test again:

1
2
3
4
5
6
7
8
9
# Running:

.S.

Finished in 0.003375s, 888.8889 runs/s, 296.2963 assertions/s.

3 runs, 1 assertions, 0 failures, 0 errors, 1 skips

You have skipped tests. Run with --verbose for details.

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
def test_something_else
  assert_equal 1, 2 - 1
end

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
gem 'minitest', '~> 5.0'
require 'minitest/autorun'
require 'minitest/pride'

class UnicornTest < Minitest::Test
end

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
  end
end

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
Unicorn.new("Robert")

Let’s add some code to the test with that behavior:

1
2
3
def test_it_has_a_name
  Unicorn.new("Robert")
end

This test isn’t asserting anything. It’s just creating a new unicorn.

We need to make an assertion:

1
2
3
4
def test_it_has_a_name
  Unicorn.new("Robert")
  assert_equal expected_value, actual_value
end

So what is the expected_value here? Well, we called the unicorn "Robert", so that would be our expected value.

1
2
3
4
def test_it_has_a_name
  Unicorn.new("Robert")
  assert_equal "Robert", actual_value
end

Now, what is actual_value? How do we ask a unicorn about its name? Probably like this:

1
unicorn.name

So in the test:

1
2
3
4
def test_it_has_a_name
  Unicorn.new("Robert")
  assert_equal "Robert", unicorn.name
end

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
def test_it_has_a_name
  unicorn = Unicorn.new("Robert")
  assert_equal "Robert", unicorn.name
end

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
gem 'minitest', '~> 5.0'
require 'minitest/autorun'
require 'minitest/pride'

class UnicornTest < Minitest::Test
  def test_it_has_a_name
    unicorn = Unicorn.new("Robert")
    assert_equal "Robert", unicorn.name
  end
end

Run the tests:

Terminal

$
ruby unicorn_test.rb
1
2
3
4
5
6
7
8
9
10
11
12
# Running:

E

Fabulous run in 0.001640s, 609.7561 runs/s, 0.0000 assertions/s.

  1) Error:
UnicornTest#test_it_has_a_name:
NameError: uninitialized constant UnicornTest::Unicorn
    unicorn_test.rb:7:in `test_it_has_a_name'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

The lonely E means error, and the actual error that we received is a NameError:

1
NameError: uninitialized constant UnicornTest::Unicorn

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 'unicorn'

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
gem 'minitest', '~> 5.0'
require 'minitest/autorun'
require 'minitest/pride'
require_relative 'unicorn'

class UnicornTest < Minitest::Test
  def test_it_has_a_name
    unicorn = Unicorn.new("Robert")
    assert_equal "Robert", unicorn.name
  end
end

Did the Unicorn Magically Appear?

Let’s run our test suite. It blows up!

1
2
unicorn_test.rb:4:in `require_relative': cannot load such file -- /Users/you/code/unicorn/unicorn (LoadError)
  from unicorn_test.rb:4:in `<main>'

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
$ touch unicorn.rb

Now we are back to the same error as before (NameError):

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

E

Fabulous run in 0.001814s, 551.2679 runs/s, 0.0000 assertions/s.

  1) Error:
UnicornTest#test_it_has_a_name:
NameError: uninitialized constant UnicornTest::Unicorn
    unicorn_test.rb:8:in `test_it_has_a_name'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

Defining the Unicorn Class

We can fix the name error my creating an empty class inside the new unicorn.rb file:

1
2
class Unicorn
end

A New Error!

Run the tests again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Running:

E

Fabulous run in 0.001844s, 542.2993 runs/s, 0.0000 assertions/s.

  1) Error:
UnicornTest#test_it_has_a_name:
ArgumentError: wrong number of arguments(1 for 0)
    unicorn_test.rb:8:in `initialize'
    unicorn_test.rb:8:in `new'
    unicorn_test.rb:8:in `test_it_has_a_name'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

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
def initialize
end

We need to make it so our initialize method accepts the argument:

1
2
3
4
class Unicorn
  def initialize(name)
  end
end

What’s in a .name?

Run the tests.

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

E

Fabulous run in 0.003082s, 324.4646 runs/s, 0.0000 assertions/s.

  1) Error:
UnicornTest#test_it_has_a_name:
NoMethodError: undefined method `name' for #<Unicorn:0x007f85f9bddfb8>
    unicorn_test.rb:9:in `test_it_has_a_name'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

Read the error:

1
NoMethodError: undefined method `name'

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
class Unicorn
  def initialize(name)
  end

  def name
  end
end

Run the tests again.

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

F

Fabulous run in 0.026091s, 38.3274 runs/s, 38.3274 assertions/s.

  1) Failure:
UnicornTest#test_it_has_a_name [unicorn_test.rb:9]:
Expected: "Robert"
  Actual: nil

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
So we have a failure. We expected `unicorn.name` to return “Robert”, but instead it returned nil. #### The Dummy Implementation: What’s the Easiest Way to Get This Test to Pass? Now, instead of an error, we get a failure. We’ll fix it, but only barely:
1
2
3
4
5
6
7
8
class Unicorn
  def initialize(name)
  end

  def name
    "Robert"
  end
end

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
# Running:

.

Fabulous run in 0.001872s, 534.1880 runs/s, 534.1880 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

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

 
require './unicorn'

The ./ part means "Look in the current directory for a file named unicorn.rb".

IRB

 
 
 
 
Unicorn.new("Forrest").name=> "Robert"Unicorn.new("Damien").name=> "Robert"

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    unicorn = Unicorn.new("Robert")
    assert_equal "Robert", unicorn.name
  end

  def test_it_can_be_named_something_else
    unicorn = Unicorn.new("Joseph")
    assert_equal "Joseph", unicorn.name
  end
end

As expected, it fails.

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

F.

Fabulous run in 0.026494s, 75.4888 runs/s, 75.4888 assertions/s.

  1) Failure:
UnicornTest#test_it_can_be_named_something_else [unicorn_test.rb:14]:
Expected: "Joseph"
  Actual: "Robert"

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

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
class Unicorn
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

That gets the test passing:

1
2
3
4
5
6
7
# Running:

..

Fabulous run in 0.001821s, 1098.2976 runs/s, 1098.2976 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ...
  end

  def test_it_can_be_named_something_else
    # ...
  end

  def test_it_is_white_by_default
    unicorn = Unicorn.new("Margaret")
    assert_equal "white", unicorn.color
  end
end

Unicorns Have No Color?

Running the test gives us an error:

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

E..

Fabulous run in 0.002155s, 1392.1114 runs/s, 928.0742 assertions/s.

  1) Error:
UnicornTest#test_it_is_white_by_default:
NoMethodError: undefined method `color' for #<Unicorn:0x007f82ecc96cf8 @name="Margaret">
    unicorn_test.rb:19:in `test_it_is_white_by_default'

3 runs, 2 assertions, 0 failures, 1 errors, 0 skips

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
class Unicorn
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def color
  end
end

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
# Running:

.F.

Fabulous run in 0.025715s, 116.6634 runs/s, 116.6634 assertions/s.

  1) Failure:
UnicornTest#test_it_is_white_by_default [unicorn_test.rb:19]:
Expected: "white"
  Actual: nil

3 runs, 3 assertions, 1 failures, 0 errors, 0 skips

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
class Unicorn
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def color
    "white"
  end
end

That gets the test passing.

1
2
3
4
5
6
7
# Running:

...

Fabulous run in 0.004366s, 687.1278 runs/s, 687.1278 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

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
unicorn.white?
# true or false

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ...
  end

  def test_it_can_be_named_something_else
    # ...
  end

  def test_it_is_white_by_default
    # ...
  end

  def test_it_knows_if_it_is_white
    unicorn = Unicorn.new("Elisabeth")
    assert_equal true, unicorn.white?
  end
end

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
def test_it_knows_if_it_is_white
  unicorn = Unicorn.new("Elisabeth")
  assert unicorn.white?
end

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
# Running:

...E

Fabulous run in 0.002220s, 1801.8018 runs/s, 1351.3514 assertions/s.

  1) Error:
UnicornTest#test_it_knows_if_it_is_white:
NoMethodError: undefined method `white?' for #<Unicorn:0x007f95749d0880 @name="Elisabeth", @color="white">
    unicorn_test.rb:24:in `test_it_knows_if_it_is_white'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

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
class Unicorn
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def color
    "white"
  end

  def white?
  end
end

Running the test again gives us new output. A failure this time:

1
2
3
4
5
6
7
8
9
10
11
# Running:

.F..

Fabulous run in 0.002242s, 1784.1213 runs/s, 1784.1213 assertions/s.

  1) Failure:
UnicornTest#test_it_knows_if_it_is_white [unicorn_test.rb:24]:
Failed assertion, no message given.

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

When we were using assert_equal we got output that said:

1
2
Expected: "Robert"
  Actual: nil

Instead, it says:

1
Failed assertion, no message given.

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
def test_it_knows_if_it_is_white
  unicorn = Unicorn.new("Elisabeth")
  assert unicorn.white?, "Elisabeth should be white, but isn't."
end

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
# Running:

F...

Fabulous run in 0.002331s, 1716.0017 runs/s, 1716.0017 assertions/s.

  1) Failure:
UnicornTest#test_it_knows_if_it_is_white [unicorn_test.rb:24]:
Elisabeth should be white, but isn't.

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

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
def white?
end

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
class Unicorn
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def color
    "white"
  end

  def white?
    true
  end
end

Run the tests and they should pass.

1
2
3
4
5
6
7
# Running:

....

Fabulous run in 0.006829s, 585.7373 runs/s, 585.7373 assertions/s.

4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ...
  end

  def test_it_can_be_named_something_else
    # ...
  end

  def test_it_is_white_by_default
    # ...
  end

  def test_it_does_not_have_to_be_white
    unicorn = Unicorn.new("Barbara", "purple")
    assert_equal "purple", unicorn.color
  end
end

This blows up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Running:

..E.

Fabulous run in 0.003224s, 1240.6948 runs/s, 1550.8685 assertions/s.

  1) Error:
UnicornTest#test_it_does_not_have_to_be_white:
ArgumentError: wrong number of arguments (2 for 1)
    /Users/you/code/unicorn/unicorn.rb:2:in `initialize'
    unicorn_test.rb:25:in `new'
    unicorn_test.rb:25:in `test_it_does_not_have_to_be_white'

4 runs, 5 assertions, 0 failures, 1 errors, 0 skips

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
Unicorn.new("Barbara", "purple")

What it expects:

1
2
3
def initialize(name)
  # ...
end

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
class Unicorn
  def initialize(name, color)
    @name = name
  end

  def name
    @name
  end

  def color
    "white"
  end

  def white?
    true
  end
end

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
# Running:

EEFEE

Fabulous run in 0.043899s, 113.8978 runs/s, 22.7796 assertions/s.

  1) Error:
UnicornTest#test_it_can_be_named_something_else:
ArgumentError: wrong number of arguments (1 for 2)
    /Users/kytrinyx/gschool/here-comes-the-unicorn/unicorn.rb:4:in `initialize'
    unicorn_test.rb:13:in `new'
    unicorn_test.rb:13:in `test_it_can_be_named_something_else'


  2) Error:
UnicornTest#test_it_is_white_by_default:
ArgumentError: wrong number of arguments (1 for 2)
    /Users/kytrinyx/gschool/here-comes-the-unicorn/unicorn.rb:4:in `initialize'
    unicorn_test.rb:18:in `new'
    unicorn_test.rb:18:in `test_it_is_white_by_default'


  3) Failure:
UnicornTest#test_it_does_not_have_to_be_white [unicorn_test.rb:29]:
Expected: "purple"
  Actual: "white"


  4) Error:
UnicornTest#test_it_has_a_name:
ArgumentError: wrong number of arguments (1 for 2)
    /Users/kytrinyx/gschool/here-comes-the-unicorn/unicorn.rb:4:in `initialize'
    unicorn_test.rb:8:in `new'
    unicorn_test.rb:8:in `test_it_has_a_name'


  5) Error:
UnicornTest#test_it_knows_if_it_is_white:
ArgumentError: wrong number of arguments (1 for 2)
    /Users/kytrinyx/gschool/here-comes-the-unicorn/unicorn.rb:4:in `initialize'
    unicorn_test.rb:23:in `new'
    unicorn_test.rb:23:in `test_it_knows_if_it_is_white'

5 runs, 1 assertions, 1 failures, 4 errors, 0 skips

What just happened? Look at the summaries:

1
EEFEE
1
5 runs, 1 assertions, 1 failures, 4 errors, 0 skips

Not a single test is passing. Let’s look at all those errors:

1
2
3
4
5
6
7
8
9
10
11
  1) Error:
ArgumentError: wrong number of arguments (1 for 2)

  2) Error:
ArgumentError: wrong number of arguments (1 for 2)

  4) Error:
ArgumentError: wrong number of arguments (1 for 2)

  5) Error:
ArgumentError: wrong number of arguments (1 for 2)

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
def initialize(name, color)
  @name = name
end

Let’s look at our tests:

1
2
3
4
5
6
7
8
9
10
11
# test 1
unicorn = Unicorn.new("Robert")

# test 2
unicorn = Unicorn.new("Joseph")

# test 3
unicorn = Unicorn.new("Margaret")

# test 4
unicorn = Unicorn.new("Elisabeth")

We’re in a bit of a bind. Most of the tests need this:

1
2
3
def initialize(name)
  @name = name
end

But one of the tests needs this:

1
2
3
def initialize(name, color)
  @name = name
end

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
class Unicorn
  def initialize(name, color="purple")
    @name = name
  end

  def name
    @name
  end

  def color
    "white"
  end

  def white?
    true
  end
end

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
# Running:

....F

Fabulous run in 0.029346s, 170.3810 runs/s, 170.3810 assertions/s.

  1) Failure:
UnicornTest#test_it_does_not_have_to_be_white [unicorn_test.rb:29]:
Expected: "purple"
  Actual: "white"

5 runs, 5 assertions, 1 failures, 0 errors, 0 skips

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
class Unicorn
  def initialize(name, color="default color")
    @name = name
    @color = color
  end

  def name
    @name
  end

  def color
    @color
  end

  def white?
    true
  end
end

Now, run the tests, and we get a single failure:

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

F....

Fabulous run in 0.032192s, 155.3181 runs/s, 155.3181 assertions/s.

  1) Failure:
UnicornTest#test_it_is_white_by_default [unicorn_test.rb:19]:
Expected: "white"
  Actual: "default color"

5 runs, 5 assertions, 1 failures, 0 errors, 0 skips

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
class Unicorn
  def initialize(name, color="white")
    @name = name
    @color = color
  end

  def name
    @name
  end

  def color
    @color
  end

  def white?
    true
  end
end

This gets all the tests passing.

1
2
3
4
5
6
7
# Running:

.....

Fabulous run in 0.002299s, 2174.8586 runs/s, 2174.8586 assertions/s.

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ..
  end

  def test_it_can_be_named_something_else
    # ..
  end

  def test_it_is_white_by_default
    # ..
  end

  def test_it_knows_if_it_is_white
    # ..
  end

  def test_it_does_not_have_to_be_white
    # ..
  end

  def test_it_knows_if_it_is_not_white
    unicorn = Unicorn.new("Roxanne", "green")
    assert_equal false, unicorn.white?
  end
end

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
def test_it_knows_if_it_is_not_white
  unicorn = Unicorn.new("Roxanne", "green")
  refute unicorn.white?
end

Run the tests, and the last one will fail:

1
2
3
4
5
6
7
8
9
10
11
# Running:

.F....

Fabulous run in 0.004108s, 1460.5648 runs/s, 1460.5648 assertions/s.

  1) Failure:
UnicornTest#test_it_knows_if_it_is_not_white [unicorn_test.rb:34]:
Failed refutation, no message given

6 runs, 6 assertions, 1 failures, 0 errors, 0 skips

Adding a Message

If we had been using assert_equal, the error message would have been:

1
2
Expected: false
  Actual: true

Now we have a message that says:

1
Failed refutation, no message given

It’s expecting white? to return false, but the return value is hard-coded:

1
2
3
def white?
  true
end

Clearly that’s not going to work.

As with assert, we can also give it a message:

1
2
3
4
def test_it_knows_if_it_is_not_white
  unicorn = Unicorn.new("Roxanne", "green")
  refute unicorn.white?, "I guess Roxanne thinks she's white, when really she is green."
end

Run it:

1
2
3
4
5
6
7
8
9
10
11
# Running:

..F...

Fabulous run in 0.004489s, 1336.6006 runs/s, 1336.6006 assertions/s.

  1) Failure:
UnicornTest#test_it_knows_if_it_is_not_white [unicorn_test.rb:34]:
I guess Roxanne thinks she's white, when really she is green.

6 runs, 6 assertions, 1 failures, 0 errors, 0 skips

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
class Unicorn
  attr_reader :name

  def initialize(name, color="white")
    @name = name
    @color = color
  end

  def color
    @color
  end

  def white?
    color == "white"
  end
end

This gets the test passing:

1
2
3
4
5
6
7
# Running:

......

Fabulous run in 0.002240s, 2678.5714 runs/s, 2678.5714 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ..
  end

  def test_it_can_be_named_something_else
    # ..
  end

  def test_it_is_white_by_default
    # ..
  end

  def test_it_knows_if_it_is_white
    # ..
  end

  def test_it_does_not_have_to_be_white
    # ..
  end

  def test_it_knows_if_it_is_not_white
    # ..
  end

  def test_unicorn_says_sparkly_stuff
    unicorn = Unicorn.new("Johnny")
    assert_equal "**;* Wonderful! **;*", unicorn.say("Wonderful!")
  end
end

Running the Tests

The test blows up:

1
2
3
4
5
6
7
8
9
10
11
12
# Running:

...E...

Fabulous run in 0.002682s, 2609.9925 runs/s, 2237.1365 assertions/s.

  1) Error:
UnicornTest#test_unicorn_says_sparkly_stuff:
NoMethodError: undefined method `say' for #<Unicorn:0x007ff5fa4103a0 @name="Johnny", @color="white">
    unicorn_test.rb:39:in `test_unicorn_says_sparkly_stuff'

7 runs, 6 assertions, 0 failures, 1 errors, 0 skips

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
class Unicorn
  attr_reader :name

  def initialize(name, color="white")
    @name = name
    @color = color
  end

  def color
    @color
  end

  def white?
    color == "white"
  end

  def say
  end
end

Adding the Parameter

The test still isn’t happy, though:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Running:

E......

Fabulous run in 0.002149s, 3257.3290 runs/s, 2791.9963 assertions/s.

  1) Error:
UnicornTest#test_unicorn_says_sparkly_stuff:
ArgumentError: wrong number of arguments (1 for 0)
    /Users/kytrinyx/gschool/here-comes-the-unicorn/unicorn.rb:17:in `say'
    unicorn_test.rb:39:in `test_unicorn_says_sparkly_stuff'

7 runs, 6 assertions, 0 failures, 1 errors, 0 skips

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
unicorn.say("Wonderful!")

And here’s how the say method is defined:

1
2
def say
end

The test is right, the code is wrong. Let’s make the say method accept an argument:

1
2
def say(something)
end

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
# Running:

......F

Fabulous run in 0.033604s, 208.3085 runs/s, 208.3085 assertions/s.

  1) Failure:
UnicornTest#test_unicorn_says_sparkly_stuff [unicorn_test.rb:39]:
Expected: "**;* Wonderful! **;*"
  Actual: nil

7 runs, 7 assertions, 1 failures, 0 errors, 0 skips

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
def say(something)
  "**;* Wonderful! **;*"
end

Run the tests:

1
2
3
4
5
6
7
# Running:

.......

Fabulous run in 0.004330s, 1616.6282 runs/s, 1616.6282 assertions/s.

7 runs, 7 assertions, 0 failures, 0 errors, 0 skips

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
class UnicornTest < Minitest::Test
  def test_it_has_a_name
    # ..
  end

  def test_it_can_be_named_something_else
    # ..
  end

  def test_it_is_white_by_default
    # ..
  end

  def test_it_knows_if_it_is_white
    # ..
  end

  def test_it_does_not_have_to_be_white
    # ..
  end

  def test_it_knows_if_it_is_not_white
    # ..
  end

  def test_unicorn_says_sparkly_stuff
    # ..
  end

  def test_unicorn_says_different_sparkly_stuff
    unicorn = Unicorn.new("Francis")
    assert_equal "**;* I don't like you very much. **;*", unicorn.say("I don't like you very much.")
  end
end

Now we get a failing test, thankfully:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Running:

......F.

Fabulous run in 0.042625s, 187.6833 runs/s, 187.6833 assertions/s.

  1) Failure:
UnicornTest#test_unicorn_says_different_sparkly_stuff [unicorn_test.rb:44]:
--- expected
+++ actual
@@ -1 +1 @@
-"**;* I don't like you very much. **;*"
+"**;* Wonderful! **;*"


8 runs, 8 assertions, 1 failures, 0 errors, 0 skips

Using String Interpolation

We can make it pass by interpolating the argument into the string with the ASCII sparkles:

1
2
3
def say(something)
  "**;* #{something} **;*"
end

This gets the final test passing.

1
2
3
4
5
6
7
# Running:

........

Fabulous run in 0.003738s, 2140.1819 runs/s, 2140.1819 assertions/s.

8 runs, 8 assertions, 0 failures, 0 errors, 0 skips

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
class Unicorn
  attr_reader :name

  def initialize(name, color="white")
    @name = name
    @color = color
  end

  def color
    @color
  end

  def white?
    color == "white"
  end

  def say(something)
    "**;* #{something} **;*"
  end
end

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
gem 'minitest', '~> 5.0'
require 'minitest/autorun'
require 'minitest/pride'
require_relative 'unicorn'

class UnicornTest < Minitest::Test
  def test_it_has_a_name
    unicorn = Unicorn.new("Robert")
    assert_equal "Robert", unicorn.name
  end

  def test_it_can_be_named_something_else
    unicorn = Unicorn.new("Joseph")
    assert_equal "Joseph", unicorn.name
  end

  def test_it_is_white_by_default
    unicorn = Unicorn.new("Margaret")
    assert_equal "white", unicorn.color
  end

  def test_it_knows_if_it_is_white
    unicorn = Unicorn.new("Elisabeth")
    assert unicorn.white?, "Elisabeth should be white, but isn't."
  end

  def test_it_does_not_have_to_be_white
    unicorn = Unicorn.new("Barbara", "purple")
    assert_equal "purple", unicorn.color
  end

  def test_it_knows_if_it_is_not_white
    unicorn = Unicorn.new("Roxanne", "green")
    refute unicorn.white?, "I guess Roxanne thinks she's white, when really she is green."
  end

  def test_unicorn_says_sparkly_stuff
    unicorn = Unicorn.new("Johnny")
    assert_equal "**;* Wonderful! **;*", unicorn.say("Wonderful!")
  end

  def test_unicorn_says_different_sparkly_stuff
    unicorn = Unicorn.new("Francis")
    assert_equal "**;* I don't like you very much. **;*", unicorn.say("I don't like you very much.")
  end
end
Feedback

Have Feedback?

Did you find an error? Something confusing? We'd love your help:

Thanks!