Rails App

Phase Twelve - Testing with RSpec

In the long run, chances are you (or someone else) will need to upgrade or fix your application. Chances are you also won’t remember all the details of your application in a year when (and trust me, it’s when not if ) you need to fix something. One of the biggest challenges to these fixes is that you can sometimes fix one part of you application, but break another.

So how do you guard against this? One way is to write “tests” that allow you not only to state the intent of your code, but also to automate the system validating that the code does what you intend.

If you find yourself inheriting an application, writing tests is a good way to figure out what is going on in the application. When you change something you can update the application, you can also help ensure you don’t break something else.

RSpec

For tests, I really like a a framework named RSpec which follows a “Behavior Driven Development” (BDD) methodology. What this means is that the tests describe a particular behavior in a test that is expressed in the code base.

Setup

First we need to add the rspec dependency to the testing environment for our app. Open the Gemfile and add the line:

gem 'rspec-rails', '~> 3.1.0', group: [:development, :test]

Now run bundle in the terminal (without production).

$ bundle --without production

Powertip: bundle is an alias for bundle install. Hey, it saves typing an extra 8 characters…

Now we can initialize rspec for our application.

$ bin/rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

This adds the configuration needed for the application, but we also want to add a Rails 4 bin stub:

$ bundle binstubs rspec-core

Edit the .rspec file to delete the --warning line. We want the output to show issues with just our code and not include all of the gems we are using. The .rspec file should look like this when done:

--color
--require spec_helper

First Test

Let’s write a test for our Transcription object. Rspec runs tests in the spec directory that are suffixed with _spec.rb. Let’s write a test for our Transcription model. Create a new file spec/models/transcription_spec.rb (you will have to create the models directory to make the path function. In it, add the following:

require "rails_helper"

describe Transcription do

  it "has a title" do
    transcript = Transcription.new(:title => "United States Constitution")
    expect(transcript.title).to eq("United States Constitution")
  end

  it "has a description" do
      transcript = Transcription.new(:description => "United States Constitution")
      expect(transcript.description).to eq("United States Constitution")
  end

  context "with 2 or more comments" do
    it "orders them in chronological order" do
      transcript = Transcription.create!
      comment1 = transcript.comments.create!(:user_name => "foo", :body => "first comment")
      comment2 = transcript.comments.create!(:user_name => "foo", :body => "second comment")

      expect(transcript.reload.comments).to eq([comment1, comment2])
    end
  end
end

Now you can run the test with bin/rspec spec.

$ bin/rspec spec
...
Finished in 0.03482 seconds (files took 1.19 seconds to load)
3 examples, 0 failures

Try adding a test that tests to ensure a transcription can be added to the application.

Controllers

You can also test controllers. This is useful to make sure the correct templates are being rendered for a given controller action. Let’s create a new file at spec/controllers/transcription_spec.rb and add the following tests.

require 'rails_helper'

describe TranscriptionsController do
  describe "GET 'index'" do
    it "returns http success header" do
      get :index
      expect(response).to be_success
      expect(response).to have_http_status(200)
    end

    it "renders the index template" do
      get :index
      expect(response).to render_template("index")
    end

    it "loads all of the transcriptions into @transcriptions" do
        transcript1, transcript2 = Transcription.create!, Transcription.create!
        get :index

        expect(assigns(:transcriptions)).to match_array([transcript1, transcript2])
    end
  end
end

Now you can run the tests:

$ bin/rspec spec
......
Finished in 0.05028 seconds (files took 1.15 seconds to load)
6 examples, 0 failures

Summary

A very common way (once you’ve gotten used to the syntax of tests) is actually to write a test before you write any code. Think of this as writing an outline like you would for a paper. You outline what you expect to cover in your paper, then go and write the actual paper. This same technique goes for software, but you have the additional benefit of having written a rhubric to judge if the code actually follows what you’re intending.

Testing does get quite complex very quickly, and it may seem like it’s not worth the time. Trust me, it is. Down the road, not only do the tests provide safeguards against accidently breaking code, you also have additional documentation on how to actually use the code. Software writers, as well intentioned as they are, often forget to update documentation, or will “get to it later.” It does take a bit of discipline, and a lot of swearing when it doesn’t work, but it will save you time down the road.