Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DRY controller specs with RSpec

I'm currently struggling a bit trying to keep my controller specs DRY and succinct and down to one assertion per example. I'm running into some difficulties particularly with where to place the actual controller request call within a structure nested to match the various edge cases.

Here's an example, simplified to demonstrate the problem:

describe MyController do
  let(:item) { Factory(:item) }
  subject { response }

  describe "GET #show" do
    before(:each) do
      get :show
    end

    context "published item" do
      it { should redirect_to(success_url) }
    end

    context "unpublished item" do
      before(:each) do
        item.update_attribute(published: false)
      end

      it { should redirect_to(error_url) }
    end
  end
end

Clearly this is a contrived example, but it illustrates what I'd like to do and what's not working. Mainly, the before block under the "unpublished" context is the problem. What happens is the change I made to the setup data actually happens after the get call due to the way the contexts are nested, so the example in that context is actually working with the initial scenario rather than the one I intend.

I understand why this happens and how contexts nest. I guess what I'd like to have is some way to tell RSpec what I'd like it to run right after any before hooks yet right before any assertions within a given context. This would be perfect for controller specs. I'd like to take advantage of nesting in my controller specs to gradually build up variations of edge cases without having to scatter the get call or even a call to a do_get helper into each of my it assertions. This would especially get annoying to keep in sync with any custom it_should macros I'm using.

Is there anything in RSpec currently to accomplish this? Are there any tricks I can use to get close? It would seem perfectly suited to the way I've seen a lot of people writing their controller specs; from what I've found, people have basically settled for having do_get helpers called before every assertion. Is there a better way?

like image 828
Chris Vincent Avatar asked Feb 13 '12 20:02

Chris Vincent


2 Answers

The DRY principle states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." What you're doing is much more about saving a few characters here and there than keeping things DRY, and the result is a tangled web of dependencies up and down a hierarchy that, as you can see, is a bitch to get to do what you want it to and consequently fragile and brittle.

Let's start with what you've got written out in a way that's verbose and works:

describe MyController do
  describe "GET #show" do
    context "published item" do
      it "redirects to the success url" do
        item = Factory(:item, published: true)
        get :show, :id => item.id
        response.should redirect_to success_url
      end
    end

    context "unpublished item" do
      it "redirects to the error url" do
        item = Factory(:item, published: false)
        get :show, :id => item.id
        response.should redirect_to error_url
      end
    end
  end
end

Now the only "pieces of knowledge" that are being duplicated are the names of the examples, which could be generated by the matchers at the end of each example. This can be resolved in a readable way by using the example method, which is an alias of it:

describe MyController do
  describe "GET #show" do
    context "published item" do
      example do
        item = Factory(:item, published: true)
        get :show, :id => item.id
        response.should redirect_to success_url
      end
    end

    context "unpublished item" do
      example do
        item = Factory(:item, published: false)
        get :show, :id => item.id
        response.should redirect_to error_url
      end
    end
  end
end

There. DRY. And quite readable and easy to change. Now, when you happen to add more examples for either of the contexts, you can add a let:

describe MyController do
  describe "GET #show" do
    context "published item" do
      let(:item) { Factory(:item, published: true) }
      example do
        get :show, :id => item.id
        response.should redirect_to success_url
      end

      example do
        # other example
      end
    end
    # ...
  end
end

Now the only duplicated code (not the same as the DRY principle) is the get. If you really feel strongly about it, you can delegate those calls out to a method like get_show(id) or some such, but it's not really buying much at that point. It's not like the API for get is going to change from under you, and the only argument to get is the item's id, which you actually care about in the example (so there's no unnecessary information).

As for using subject to capture the response and get one-liners out of the deal, that just makes things really difficult to read and doesn't save you much. In fact, I've come to consider using subject in this way to be a smell.

Hope this all helps.

Cheers, David

like image 112
David Chelimsky Avatar answered Sep 19 '22 12:09

David Chelimsky


Will

context "unpublished item" do
  let(:item) do
    Factory(:item, published: false)
  end

  it { should redirect_to(error_url) }
end

work for you? BTW, before by default is before(:each) so you can DRY you specs a little more.

UPDATE: you can also isolate examples with anonymous contexts, like:

describe "GET #show" do
  let(:show!) do
    get :show
  end

  context do
    before { show! }

    context "published item" do
      it { should redirect_to(success_url) }
    end 

    # another examples with show-before-each
  end

  context "unpublished item" do
    before do
      item.update_attribute(published: false)
      show!
    end

    it { should redirect_to(error_url) }
  end
end
like image 22
Nash Bridges Avatar answered Sep 23 '22 12:09

Nash Bridges