Prior to the introduction of nested example groups in RSpec, I'd always felt that descriptions got a little unwieldy when trying to describe the different cases and disliked specifying the
controller_name repeatedly. For example (and these are real examples from real projects at work, with actual code removed for conciseness):
describe 'POST HotelDealsController#create with a valid hotel deal' do controller_name :hotel_deals before(:each) do # ... end it "should create and save a new hotel deal" do # ... end end describe 'POST HotelDealsController#create with an invalid hotel deal' do controller_name :hotel_deals before(:each) do # ... end it "should not perform any redirection" do # ... end end
Nested example groups allow me to do this instead:
describe SearchController do before(:each) do @search = mock_model(Search) end describe "POST 'create'" do it "should render 'new' when creating an invalid search" do # ... end describe "with a valid search that has a location_id" do before(:each) do @search.stub!(:valid?).and_return(true) @search.stub!(:location_id).and_return(1) end it "should save a new search that has a location_id, location_code and location_name, and redirect to 'show'" do # ... end end describe "with a valid search that doesn't have a location_id" do before(:each) do @search.stub!(:valid?).and_return(true) @search.stub!(:location_id).and_return(nil) end it "should ask the Location model for possible locations if the search doesn't have a location_id" do # ... end end end end
What's the difference you say? Well, while it may sound trivial, I can do
describe SearchController only once and nest all examples for the different actions and scenarios inside without breaking it up into separate top-level example groups where I'd need to say
controller_name :search multiple times.
Another (more important) benefit is I can progessively specify different scenarios I want to test by nesting them and providing a
before block to setup mocks and stubs for that specific scenario. Let's look at what I mean with some code. Over here, I'm specifying the
create action of my
describe "POST 'create'" do it "should render 'new' when creating an invalid search" do # ... end describe "with a valid search that has a location_id" do # ... end describe "with a valid search that doesn't have a location_id" do # ... end end
There're 3 possible scenarios here: an invalid search, a valid search with a location_id, and a valid search without an location_id. I can write examples for invalid searches within the top-level example group itself (
it "should render 'new' when creating an invalid search"). Now, to test valid searches, I break out 2 nested example groups with their own
before block to setup mock searches, one which has a location_id, the other doesn't. What's the big deal? It just feels much more organized than the first, non-nested example where a top-level example group is created for each scenario.
And I know it may not seem like much, and I don't think there's anything particularly wrong with non-nested examples - it's a matter of personal preference. Nesting allows me to write examples for controllers in a saner fashion where I can say confidently to myself that "all examples for XXX action go here". I'm a big believer in readable tests (if you're on the Rails Trac much and have seen my reviews and patches you should know), so being able to write specs where I feel they belong makes me happier, and the examples read really nicely too.
I guess what I'm really trying to say is this: when trying to add a new example to legacy specs (legacy being anything that was written more than 5 mins ago) I instantly know where to place it - gone are the days of going through different example groups looking for the right place to put my new specification (and often settling on just simply creating a new top-level example group). I like to think we've all been there.
That said, I'm not saying that deeply nested examples are a good thing. When testing Rails controllers I find that you don't often have to go more than 3 deep to allow for all the possible scenarios. Any more would suggest that your controller is doing too much work.