RSpec3: Back in the Mega Saddle

Coming from the harrowing halls of Java EE, I was at first reticent to truly fall in love with Ruby on Rails and its associated technologies. Easy deployment? Testing Suites baked into the frameworks rather than grafted on a later date? Scalability at a moment’s notice? This shall be the start of feckless ruin!

At Tyrannosaurus Tech, we build through Behavior-Driven Development. BDD came from Test-Driven Development, and can be accurately summed up as testing from the outside in. We write behavior and specification that then drives our development. We have used RSpec 3 for our recent projects, and I have grown to love it. I am here to show you the basics of how to use it.

I would like to reiterate that this tutorial is focused on just RSpec itself. While learning RSpec from the perspective of Rails is possible, it is a steeper learning curve. If you have strong testing fundamentals, it is easy to move onto a Rails app.

Let’s start with a blank project. You will need the most recent version of Ruby, currently, 2.4.1. Install the RSpec gem via CLI by typing the command following the $ symbol (which indicates your prompt):

$ gem install rspec -v 3.6.0

Check to make sure that it worked:

$ rspec --version

In your project, make a new folder called spec. Inside spec folder, create a file titled car_spec.rb. Add the following

RSpec.describe 'A car' do
it 'has a make name' do
end
end

In BDD, you should start with an outline of your intended behaviour. You start to fill out this outline as you progress with testing. Add the highlighted lines to your code:

RSpec.describe 'A car' do
it 'has a make name' do
car = Car.new('ford', 0)
make = car.make
expect(make).to eq('ford')
end
end

Before you run this, let’s talk about what is going on here.

The outer RSpec.describe block creates an example group. This is what defines what you’re testing.
The nested inner block, the one that starts with it 'has a model name' do, is an example of the car’s use. When you write specs, try to make each example focused on specific behaviour that you’re testing.

Thus, it is time to intone the mantra of AAA, Arrange / Act / Assert:

  • Set up an object.
  • Do something with it
  • Check that it behaved the way you wanted

In this example, you create a Car, ask for its make, and confirm that the make is `ford`. The line that starts with expect is an expectation. They are like assertions in other test frameworks, but can do much more. Now that we have the building blocks down, let us review what each part means:

  • RSpec.describe creates an example group (a set of tests)
  • it creates an example (individual test)
  • expect confirms the expected outcome (assertion)

Specs do two things for your project. A well written set of tests serves as documentation for what your model and/or feature should do, and that it does what it is supposed to do. Let’s go ahead and run RSpec from your project directory, by typing rspec in at your command line prompt. You should get the following failure:

$ rspec
F

Failures:

1) A car has a make
Failure/Error: car = Car.new('ford', 0)

NameError:
uninitialized constant Car
# ./spec/car_spec.rb:7:in `block (2 levels) in '

Finished in 0.00662 seconds (files took 0.27775 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/car_spec.rb:6 # A car has a make

RSpec gives us a report that shows which spec failed, the line where the error occurred, and a brief description of the problem. You should always be aware of the colors that are printed out.

  • Passing specs are green
  • Failures and their details are red
  • Examples descriptions and their accompanying text is black.
  • Extra details such as stack traces are blue
  • Pending specs will be yellow

Using our pattern of AAA, we have asserted our outline. Now we need to make it pass. Let’s go ahead and make our Car model. In your project root, create a folder titled lib. Inside this lib folder, place the following code in a new file, saved as car.rb:

class Car
attr_reader :make, :miles

def initialize(make, miles)
@make = make
@miles = miles
end
end

In your car_spec.rb file, add the following line to the top:

require 'car'

Your tests should now pass. The methods describe, it, and expect are the core of RSpec. You can go a long way with just these. There are still some other helpful methods that you should know about. As you write more specs, you are bound to repeat yourself. RSpec provides a way to share your setup with your examples. Add the following block after the first inner it block:

it 'lets me add miles' do
car = Car.new('ford', 0)
car.miles = 100;
miles = car.miles;
expect(miles).not_to be < 99;
end

Run it. While this spec is fine, it repeats itself. We can do better via hooks. Hooks run automatically at specific times during tests. The very first hook we will try is before, which runs before each example. Add this line to the file, inside of the RSpec.describe block and before the inner it blocks:

before{ @car = Car.new('ford', 0)}

This will run before each example. The @car variable is now ready for use. Note that the individual instance of the car is not shared and that each example get it’s own car. Thus, you can change things around in one example without fear of it impacting the others. You will now need to replace the car variable with @car:

RSpec.describe 'A car' do

before{ @car = Car.new('ford', 0)}

it 'has a make' do
make = @car.make
expect(make).to eq('ford')
end

it 'lets me add miles' do
@car.miles = 100;
miles = @car.miles;
expect(miles).not_to be < 99;
end
end

RSpec also gives us a construct called let, which gives us nicer syntax. Undo the changes you made for the before hook. Your file should look like this:

require 'car'

RSpec.describe 'A car' do

it 'has a make' do
make = car.make
expect(make).to eq('ford')
end

it 'lets me add miles' do
car.miles = 100;
miles = car.miles;
expect(miles).not_to be < 99;
end
end

At the top of the file, below RSpec.describe, add the following line of code.

let(:car) { Car.new('ford', 0) }

Run your code. It should pass. Let is a memoized helper method that will cache the value for examples, but not across your examples. For those of you who don’t know, memoization is when we store the results of an operation and refer to the stored copy from then on.

With this, you know the important bits of RSpec. I recommend exploring the API more to find out various methods that can assist you in furthering your tests. I would like to recommend the book Effective Testing with RSpec 3 by Myron Marston and Ian Dees. While the book is still being developed, a large chunk of this blog post and my RSpec knowledge is derived from them. I frequently check back to see addendums, and various changes that helps me keep up with the framework.