new controller examples
July 1st, 2008
There’s been a lot of discussion about clarity over DRY lately. This is something that I’ve been espousing for some time, but recent posts by Jay Fields, Mikel Lindsaar and Dan North have gotten me thinking about it again with more focus.
With this in mind, I’ve been refining the examples generated for restful controllers when you run script/generate rspec_scaffold with the rspec-rails plugin. I’ve got them now where I’m pretty happy with them, but I’m curious to hear what you think. I’m not going to tell you what I changed or what to look for, I’m just going to ask you to look it over and post your comments.
There are two listings: the generated code and the output you get from running the examples. Thanks in advance for any feedback.
script/generate rspec_scaffold account
require File.expand_path(File.dirname(FILE) + '/../spec_helper')describe AccountsController do
def mock_account(stubs={}) stubs = { :save => true, :update_attributes => true, :destroy => true, :to_xml => '' }.merge(stubs) @mock_account ||= mock_model(Account, stubs) end
describe "responding to GET /accounts" do
it "should succeed" do Account.stub!(:find) get :index response.should be_success end it "should render the 'index' template" do Account.stub!(:find) get :index response.should render_template('index') end it "should find all accounts" do Account.should_receive(:find).with(:all) get :index end it "should assign the found accounts for the view" do Account.should_receive(:find).and_return([mock_account]) get :index assigns[:accounts].should == [mock_account] endend
describe “responding to GET /accounts.xml” do
before(:each) do request.env["HTTP_ACCEPT"] = "application/xml" end it "should succeed" do Account.stub!(:find).and_return([]) get :index response.should be_success end it "should find all accounts" do Account.should_receive(:find).with(:all).and_return([]) get :index end it "should render the found accounts as xml" do Account.should_receive(:find).and_return(accounts = mock("Array of Accounts")) accounts.should_receive(:to_xml).and_return("generated XML") get :index response.body.should == "generated XML" endend
describe “responding to GET /accounts/1″ do
it "should succeed" do Account.stub!(:find) get :show, :id => "1" response.should be_success end it "should render the 'show' template" do Account.stub!(:find) get :show, :id => "1" response.should render_template('show') end it "should find the requested account" do Account.should_receive(:find).with("37") get :show, :id => "37" end it "should assign the found account for the view" do Account.should_receive(:find).and_return(mock_account) get :show, :id => "1" assigns[:account].should equal(mock_account) endend
describe “responding to GET /accounts/1.xml” do
before(:each) do request.env["HTTP_ACCEPT"] = "application/xml" end it "should succeed" do Account.stub!(:find).and_return(mock_account) get :show, :id => "1" response.should be_success end it "should find the account requested" do Account.should_receive(:find).with("37").and_return(mock_account) get :show, :id => "37" end it "should render the found account as xml" do Account.should_receive(:find).and_return(mock_account) mock_account.should_receive(:to_xml).and_return("generated XML") get :show, :id => "1" response.body.should == "generated XML" endend
describe “responding to GET /accounts/new” do
it "should succeed" do get :new response.should be_success end it "should render the 'new' template" do get :new response.should render_template('new') end it "should create a new account" do Account.should_receive(:new) get :new end it "should assign the new account for the view" do Account.should_receive(:new).and_return(mock_account) get :new assigns[:account].should equal(mock_account) endend
describe “responding to GET /accounts/1/edit” do
it "should succeed" do Account.stub!(:find) get :edit, :id => "1" response.should be_success end it "should render the 'edit' template" do Account.stub!(:find) get :edit, :id => "1" response.should render_template('edit') end it "should find the requested account" do Account.should_receive(:find).with("37") get :edit, :id => "37" end it "should assign the found Account for the view" do Account.should_receive(:find).and_return(mock_account) get :edit, :id => "1" assigns[:account].should equal(mock_account) endend
describe “responding to POST /accounts” do
describe "with successful save" do it "should create a new account" do Account.should_receive(:new).with({'these' => 'params'}).and_return(mock_account) post :create, :account => {:these => 'params'} end it "should assign the created account for the view" do Account.stub!(:new).and_return(mock_account) post :create, :account => {} assigns(:account).should equal(mock_account) end it "should redirect to the created account" do Account.stub!(:new).and_return(mock_account) post :create, :account => {} response.should redirect_to(account_url(mock_account)) end end describe "with failed save" do it "should create a new account" do Account.should_receive(:new).with({'these' => 'params'}).and_return(mock_account(:save => false)) post :create, :account => {:these => 'params'} end it "should assign the invalid account for the view" do Account.stub!(:new).and_return(mock_account(:save => false)) post :create, :account => {} assigns(:account).should equal(mock_account) end it "should re-render the 'new' template" do Account.stub!(:new).and_return(mock_account(:save => false)) post :create, :account => {} response.should render_template('new') end endend
describe “responding to PUT /accounts/1″ do
describe "with successful update" do it "should find the requested account" do Account.should_receive(:find).with("37").and_return(mock_account) put :update, :id => "37" end it "should update the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:update_attributes).with({'these' => 'params'}) put :update, :id => "1", :account => {:these => 'params'} end it "should assign the found account for the view" do Account.stub!(:find).and_return(mock_account) put :update, :id => "1" assigns(:account).should equal(mock_account) end it "should redirect to the account" do Account.stub!(:find).and_return(mock_account) put :update, :id => "1" response.should redirect_to(account_url(mock_account)) end end describe "with failed update" do it "should find the requested account" do Account.should_receive(:find).with("37").and_return(mock_account(:update_attributes => false)) put :update, :id => "37" end it "should update the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:update_attributes).with({'these' => 'params'}) put :update, :id => "1", :account => {:these => 'params'} end it "should assign the found account for the view" do Account.stub!(:find).and_return(mock_account(:update_attributes => false)) put :update, :id => "1" assigns(:account).should equal(mock_account) end it "should re-render the 'edit' template" do Account.stub!(:find).and_return(mock_account(:update_attributes => false)) put :update, :id => "1" response.should render_template('edit') end endend
describe “responding to DELETE /accounts/1″ do
it "should find the account requested" do Account.should_receive(:find).with("37").and_return(mock_account) delete :destroy, :id => "37" end it "should call destroy on the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:destroy) delete :destroy, :id => "1" end it "should redirect to the accounts list" do Account.stub!(:find).and_return(mock_account) delete :destroy, :id => "1" response.should redirect_to(accounts_url) endend
end
script/spec spec/controllers/accounts_controller_spec.rb -fn
AccountsController
responding to GET /accounts
should succeed
should render the 'index' template
should find all accounts
should assign the found accounts for the view
responding to GET /accounts.xml
should succeed
should find all accounts
should render the found accounts as xml
responding to GET /accounts/1
should succeed
should render the 'show' template
should find the requested account
should assign the found account for the view
responding to GET /accounts/1.xml
should succeed
should find the account requested
should render the found account as xml
responding to GET /accounts/new
should succeed
should render the 'new' template
should create a new account
should assign the new account for the view
responding to GET /accounts/1/edit
should succeed
should render the 'edit' template
should find the requested account
should assign the found Account for the view
responding to POST /accounts
with successful save
should create a new account
should assign the created account for the view
should redirect to the created account
with failed save
should create a new account
should assign the invalid account for the view
should re-render the 'new' template
responding to PUT /accounts/1
with successful update
should find the requested account
should update the found account
should assign the found account for the view
should redirect to the account
with failed update
should find the requested account
should update the found account
should assign the found account for the view
should re-render the 'edit' template
responding to DELETE /accounts/1
should find the account requested
should call destroy on the found account
should redirect to the accounts list


July 1st, 2008 at 12:02 am
I like it. A couple comments:
might write the examples. I’ve seen several people that write them in the same order you did here, but might organize them in the opposite order. That is, they’d start with “should be_success” as the first one, but then insert the “should render_template” example above it.
story to me:
“It should show a page” “Which page?” “The index page” “What must go on the page?” “Accounts”
usually drop some of the examples. Like in the “responding to GET /accounts.xml” I would delete the “should_receive(:find)” example. That’s already covered in the other one, and I know I’m not going to do the find any differently just because it’s XML. On the other hand, I would include the “should be_success” one just to make sure the response is a 200. So I’d probably write the XML example as
describe "responding to GET /accounts.xml" do before(:each) do request.env["HTTP_ACCEPT"] = "application/xml" endit "should succeed" do Account.stub!(:find).and_return([]) get :index response.should be_success end
it "should render the found accounts as xml" do Account.stub(:find).and_return(accounts = mock("Array of Accounts")) accounts.should_receive(:to_xml).and_return("generated XML") get :index response.body.should == "generated XML" end end
I have mixed feelings about omitting an expectation to #save in the “POST to create” spec. It’s easy to read over it and think “does it actually save the object?” I actually like omitting it as you did, but I also draw attention to the save call:
describe "with successful save" do
end
As you can see, I will even do it when the stubbed methods are the same as the defaults. Having that successful_account helper method just makes explicit the fact that we’re dealing with an object that saved successfully. It immediately conveys all the context the reader needs to understand the following examples.
July 1st, 2008 at 12:02 am
Thanks for the feedback Pat. As you might imagine, I went through a few iterations of this. At one point I had something like what you have for “with successful save” (with additional
valid_accountandinvalid_accounthelper methods) and at one point I had this:it "should create a new account" do Account.should_receive(:new).with({'these' => 'params'}).and_return(mock_account(:save => true)) post :create, :account => {:these => 'params'} end
it "should assign the created account for the view" do Account.stub!(:new).and_return(mock_account(:save => true)) post :create, :account => {} assigns(:account).should equal(mock_account) end
it "should redirect to the created account" do Account.stub!(:new).and_return(mock_account(:save => true)) post :create, :account => {} response.should redirect_to(account_url(mock_account)) end
end
I think you’re right that duplicating the defaults tells the story better, so I’m inclined to go back to the additional helpers or add the bit of verbosity as in the code in this comment. Which do you (or anyone else) find more readable?
July 1st, 2008 at 12:02 am
I like the examples, but I have a comment on the ‘describe’ labels.
separate specs for routing, which I like, because routing ought to be independent of the controller. However, the specs still say things like “responding to GET /accounts”, even though they are actually calling the action methods directly. I think there are cases where the routing could be changed, but the examples would still pass, with misleading descriptions.
descriptions more like “responding to a GET on an account”, which leave the description of where the request came from to the routing spec.
July 1st, 2008 at 12:02 am
Nice David.
of my text editor and be able to understand what the blazes is going on from. If I have to resort to the mouse wheel, then I know I need to re-factor my descriptive text.
redundant and it reads better without. I also do the same thing, if I am spec’ing a javascript output, I only spec that part of the code execution part (the respond to block) knowing that the rest has been covered by previous specifications handling the http request.
the helpers….
it "should assign the created account for the view" do given_a_valid_account post :create, :account => {} assigns(:account).should equal(mock_account) end
etc...
I think the biggest advantage of using this quazi-proto-dsl in the method name ‘given_a_valid_account’ is that it really helps explain to the novice user what is going on in the test.
rails project in two weeks. He is now specing and everything, but being able to show him the specs where I put the given_a_blah type model names, he understood the spec faster and could think with it. it really made a big difference.
developer a lot to try and grok RSpec in general… so if we can make it as clear as possible, we then lower the barrier to entry.
July 1st, 2008 at 12:02 am
Reading over the examples, they didn’t feel to me like the examples would look if they were all done using TDD. In particular, I wouldn’t have examples like the show action renders the show template. That’s the default behavior in Rails – that example would have never been red. I’ll even go as far as to say I think it hurts the examples overall. If I see an example like that, I think there must be something interesting going on in the code that motivated somebody to test an implicit render. I took a quick pass over the examples and left some comments: http://pastie.org/227513
July 1st, 2008 at 12:02 am
Hey Dan – Thanks for your critique. I agree with most of your comments, though there are a couple of additions I feel are necessary to make it all hang together. Please check out the result and let me know what you think.
July 1st, 2008 at 12:02 am
Hey David – I like your latest revision much better. IMO, passing the args to mock_account gives the examples more clarity since you can tell that a specific example cares about the mock responding a certain way to certain method calls. I also feel like the proportion of the examples as a whole to the controller is much better. I’m looking forward to seeing what you end up with.
July 1st, 2008 at 12:02 am
OK – here’s another crack, taking @Kerry’s concerns into account (so to speak):
July 1st, 2008 at 12:02 am
Latest revision is nice.
July 1st, 2008 at 12:02 am
I agree with Pat Maddox on the missing of an expectation on save in POST create.
revision (which I really like). However I found it somewhat confusing to see:
Was all that I needed to get the POST create to pass. When In my mind the most important behaviour of POST create is that it attempts to create(save) something.
July 1st, 2008 at 12:02 am
@Joseph – I think Pat’s comment was about drawing attention to the save call for readability:
I don’t think it was about setting an expectation for that. Now that you point it out, I’d say it’s missing. I’ll add it.
July 1st, 2008 at 12:02 am
it "gets index" do Account.expects(:find).with(:all).returns [mock_account] get :index it_renders :index it_assigns :accounts, [mock_account] endThats 21 not very readable LOCs to 6 dead-simple.
July 1st, 2008 at 12:02 am
@grosser
July 1st, 2008 at 12:02 am
This isn’t DRY. Read this: http://jamesgolick.com/2007/10/19/introducing-resource_controller-focus-on-what-makes-your-controller-special … and never write these pointless tests again. (ie just test what you have added yourself).
July 1st, 2008 at 12:02 am
@adam – I’m not sure you understand the context of this post. The examples we’re talking about are those that rspec generates when you choose to use rails’ generators to generate your controllers. Those controllers have a wealth of untested code in them, and leaving them untested introduces a higher level of risk when you start changing them to suit your specific needs.
July 1st, 2008 at 12:02 am
@one example per test