fbpx

Blogs from the Ranch

< Back to Our Blog

RSpec Tip: let and nested describe/context blocks

At Highgroove, we test our code extensively. The best testing code has solid coverage, yet also shares many of the qualities of well-written code in general, such as readability and lack of repetition.

The popular testing framework RSpec ships with constructs that aid in writing elegant testing code. One advanced technique to keep things DRY is shown after the break.

A recent story we finished for a client specified that the author of a virtual bulletin board post could later edit that post. Implicitly, users should not be able to edit posts they do not own.

One way to write tests that exercise the PUT action for the controller that handles updating posts might look like:

describe "PUT #update" do
  it "is successful when the post's author makes the request" do
    user = FactoryGirl.create(:user)
    post = FactoryGirl.create(:post, author: user)
    sign_in(user)

    put post_path(post.id), { title: "Updated Post", body: "Typos fixed!" }

    expect(response).to be_success
  end

  it "is forbidden when a non-author makes the request" do
    user = FactoryGirl.create(:user)
    post = FactoryGirl.create(:post)
    sign_in(user)

    put post_path(post.id), { title: "Updated Post", body: "Typos fixed!" }

    expect(response).to have_http_status(403)
  end
end

And for this simple example, these tests are actually pretty solid.

However, there is some obvious repetition: both tests create a post, sign in a user (though a different user each time), and make a PUT request.

The repetitive code is a good candidate for a shared before block which runs before each test, except that the tests need to sign in a different user and the user needs to be signed in before the request is made.

With a small amount of refactoring, I came up with this:

describe "PUT #update" do
  let(:user) { FactoryGirl.create(:user) }

  before do
    sign_in(user)
    put post_path(post.id), { title: "Updated Post", body: "Typos fixed!" }
  end

  context "post author is the current user" do
    let(:post) { FactoryGirl.create(:post, author: user) }

    it "is successful" do
      expect(response).to be_success
    end
  end

  context "current user is not the post author" do
    let(:post) { FactoryGirl.create(:post) }

    it "is forbidden" do
      expect(response).to have_http_status(403)
    end
  end
end

The refactorings include:

  • Nested context blocks for grouping shared context and also providing indented output when using the documentation formatter.

  • let method for defining a memoized helper method that is lazily evaluated. Notice specifically that each context defines the user helper differently, and this works correctly even though the helper is required in the outer before block.

We are always on the look out for ways to make our tests more readable and elegant. What techniques do you use in either RSpec or another framework?

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project