TDD with Ruby

Install Homebrew

We're going to start by installing Homebrew which will make installing everything else we need much easier.

Open a Terminal window and copy and paste the following into your terminal:

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Then ensure you have the latest version of Homebrew by typing the following:

$ brew update

And finally, ensure that Homebrew's environment is in a healthy state:

$ brew doctor

This may give you some commands to run in order to fix any problems, which you should do before continuing.

Install Ruby

Now we're ready to install Ruby on our system. We'll be using Homebrew to do that.

We start by installing rbenv which is a Ruby Version Manager. This allows us to have multiple version of Ruby installed and easily switch between them. Then we’re going to install a specific version of Ruby.

$ brew install rbenv
$ rbenv install 2.2.3

Creating a Project

Let’s make a directory for our project. This is where we’ll be putting all our code.

$ mkdir piglatin-ruby
$ cd piglatin-ruby

Then we can use rbenv to specify the version of Ruby we want to use for this project:

$ rbenv local 2.2.3

This will create a file named .ruby-version that contains the string 2.2.3. This lets rbenv know what version of Ruby to use the next time you change into this directory to work on this project.

Dependency Management

When building a software project you will often use libraries created by other people. In order to both specify and automatically download these dependencies you'll want to use a dependency management system.

We’re going to set up dependency management for our project using Bundler. First we install the Bundler Gem and then we create a Gemfile to define our dependencies.

$ gem install bundler
$ bundle init

Add Testing Framework

In order to express the requirements of our code we're going to use a testing framework. Testing frameworks, at their most basic, allow you to test the output of a function when given a certain input.

Edit the generated Gemfile to bring in RSpec, the testing library we’ll be using:

source "https://rubygems.org"

gem 'rspec'

Then run bundle to install the dependencies. This will generate a Gemfile.lock which specifies the version of each dependency that we are using. This ensures that other developers on our project (including ourselves!) will get the same version when they work on our code on another computer.

Next we’ll initialize our project to use RSpec:

$ rspec --init

Then run rspec to ensure everything is set up correctly.

Our First Failing Test

Let's create a test for our first requirement - translating apple to appleway.

Create the file spec/piglatin_spec.rb with the following contents:

describe Piglatin do
  context "word startig with a vowel" do
    it "translates the word" do
      expect(Piglatin.translate_word "apple").to eq "appleway"
    end
  end
end

Here we are telling RSpec that we’d like a class named Piglatin that has a class method named translate_word and when we pass it the word apple it we expect to get back appleway.

Run your test using rspec to see it fail:

$ rspec
/Users/bkelly/temp/piglatin-ruby/spec/piglatin_spec.rb:1:in `<top (required)>': uninitialized constant Piglatin (NameError)

It’s failing because we haven’t actually created a class named Piglatin yet.

Create the Piglatin Class

Create the directory lib and add the file piglatin.rb in it with the following contents:

class Piglatin

end

Then update spec/spec_helper.rb and put the following at the top:

require File.expand_path("../../lib/piglatin", __FILE__)

This will make your new class available to the specs.

Now we run our tests again:

$ rspec
F

Failures:

  1) Piglatin words starting with a vowel translates the word correctly
     Failure/Error: expect(Piglatin.translate_word 'apple').to eq 'appleway'

     NoMethodError:
       undefined method `translate_word' for Piglatin:Class
     # ./spec/piglatin_spec.rb:4:in `block (3 levels) in <top (required)>'

Finished in 0.00044 seconds (files took 0.0952 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/piglatin_spec.rb:3 # Piglatin words starting with a vowel translates the word correctly

Create the Translate Word Method

Here it’s telling us there is no method translate_word for our class, so let’s add it:

class Piglatin
  def self.translate_word

  end
end

And we run our test yet again:

$ rspec
F

Failures:

  1) Piglatin words starting with a vowel translates the word correctly
     Failure/Error: def self.translate_word

     ArgumentError:
       wrong number of arguments (1 for 0)
     # ./lib/piglatin.rb:2:in `translate_word'
     # ./spec/piglatin_spec.rb:4:in `block (3 levels) in <top (required)>'

Finished in 0.00048 seconds (files took 0.09886 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/piglatin_spec.rb:3 # Piglatin words starting with a vowel translates the word correctly

Fix the Method Signature

Update the argument list for the method:

class Piglatin
  def self.translate_word(word)

  end
end

And then run your test again:

$ rspec
F

Failures:

  1) Piglatin words starting with a consonant translates the word correctly
     Failure/Error: expect(Piglatin.translate_word 'word').to eq 'appleway'

       expected: "appleway"
            got: nil

       (compared using ==)
     # ./spec/piglatin_spec.rb:4:in `block (3 levels) in <top (required)>'

Finished in 0.11592 seconds (files took 0.09504 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/piglatin_spec.rb:3 # Piglatin words starting with a consonant translates the word correctly

Return the Correct Answer

What’s the easiest way to make this pass? How about just returning the string appleway:

class Piglatin
  def self.translate_word(word)
    "appleway"
  end
end

Run the test once again:

$ rspec
.

Finished in 0.00079 seconds (files took 0.0962 seconds to load)
1 example, 0 failures

Yay, our first passing test!

Another Word

Let’s add a second word to our test:

describe Piglatin do
  context "word starting with a vowel" do
    it "translates the word" do
      expect(Piglatin.translate_word "apple").to eq "appleway"
      expect(Piglatin.translate_word "orange").to eq "orangeway"
    end
  end
end

We run rspec again and see one test passes and one test fail:

$ rspec
F

Failures:

  1) Piglatin words starting with a vowel translates the word correctly
     Failure/Error: expect(Piglatin.translate_word 'orange').to eq 'orangeway'

       expected: "orangeway"
            got: "appleway"

       (compared using ==)
     # ./spec/piglatin_spec.rb:5:in `block (3 levels) in <top (required)>'

Finished in 0.01019 seconds (files took 0.10086 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/piglatin_spec.rb:3 # Piglatin words starting with a vowel translates the word correctly

This is because we hardcoded our method to return appleway.

Some Actual Code

Let's add some code to actually do something with the word we are passed.

The Pig Latin rules state that when the word starts with a vowel we just add `way` to the end of it.

Since we only have tests for words starting with vowels, let's do the minimum amount of work to get our test passing - appending `way` to the word that's passed it.

class Piglatin
  def self.translate_word(word)
    word + "way"
  end
end

Then run our test again:

$ rspec
.

Finished in 0.00083 seconds (files took 0.09543 seconds to load)
1 example, 0 failures