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?
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With