This is part 1 of a 5 part series on restful controller tests, using Shoulda as the foundation. Here are all of them so far:
- The Basics
- Stubbing for Speed
After porting eight MVC stacks from an older application to its newer home, and revamping all the tests along the way, I developed a great rhythm in cranking out functional tests for restful resources. I took the time to use the best that modern shoulda has to offer, and came up with (what I feel) is a great set of functional tests.
First, each controller action gets its own context, with several tests executed in each. But wait, there’s more! Do you have multiple roles? I usually have visitor (not logged in), member (logged in, normal user) and admin (logged in, superuser). That means triple the testing, because I want to confirm that the application is behaving the correct way under each circumstance, and the behavior is often very different.
Next, a little background. For authentication this app is using restful_authentication and role_requirement. My basic testing suite consists of shoulda, factory_girl, and woulda. I’m also using autotest, mocha, test_benchmarker, and redgreen to speed up my tests and development cycle, but I’ll cover those in a different article. This is about the basics.
Let’s begin (finally) with a restful controller for site settings – admins have all access, but members and visitors have none. This will allow me to demonstrate how I handle testing each role, and it’s easy to grant more privileges to a role by copying over the tests from say, admin to member.
Admin Tests
I find it’s easier TDD to start with the least restrictive role and work backward, adding restrictions to my controller as I go along. That said, here are my admin tests. I first setup the admin context like so:
context "as admin" do
setup do
@setting = Factory.create(:setting)
@valid = Factory.build(:setting).attributes
login_as :admin
end
...
end
I’ve created a basic setting (using factories), and a hash of valid attributes I can use for create and update actions. I’ve also logged in as admin. If you don’t understand contexts and setup, a context is a group of tests, and the setup for that context is done before every test within. Contexts can also be nested – and they will be.
Index
context "getting index" do
setup do
get :index
end
should_assign_to(:settings){[@setting]}
should_respond_with :success
should_render_with_layout
should_render_template :index
should_not_set_the_flash
end
Using shoulda’s awesome macros, I setup a simple get :index which will run before each shoulda macro, then go nuts testing that @settings is assigned an array with my one and only existing setting. The page responds with success, it renders the default layout with the index template, and doesn’t set the flash. These five tests are what you should think about for every action, under every role. This is the foundation, and you add stuff from here as you add more complicated functionality to the basic scaffold.
New
context "getting new" do
setup do
get :new
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout
should_render_template :new
should_not_set_the_flash
end
This is very similar to index above. Now, I’m testing that @setting (singular) is set, with a single object of class Setting. This is as specific as I can test right now, because this was a newly created object in the action – so I can’t test that a specific setting was called. The rest is nearly identical to the index action, expect the template I expect to be rendered.
Create
context "posting create" do
context "with valid data" do
setup do
post :create, :setting => @valid.merge('name' => 'Slappy')
end
should_assign_to :setting, :class => Setting
should_redirect_to("index page"){settings_path}
should_set_the_flash_to "Your setting was successfully saved."
should "create the record" do
assert Setting.find_by_name('Slappy')
end
end
context "without valid data" do
setup do
post :create, :setting => {}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout
should_render_template :new
should_not_set_the_flash
end
end
Now we’re getting a little more complex. We have two outcomes to think about: a successful create, and a failed one. We have to react differently, so we have to test for both.
The first sub-context tests for success. We’re passing in a valid hash of attributes. Notice our tests look different this time – and easier on the fingers. Instead of 5 basic tests, we have 3. We still check that a Setting object was created/assigned, but instead of checking response codes and rendered content, we’re expecting the action to redirect to the index page. And this time, we are expecting a flash message to be set. Finally, I’m checking that the record was actually created. I forced the name attribute to be ‘Slappy’ because no matter how my test suite changes in the future, that name is unlikely to already be in the database.
The next sub-context tests for failure – most likely from invalid input on the part of the user. It happens. We’ve passed an empty attribute list to the action, which should ensure failure if we have at least one required attribute with validates_presence_of in the model. In my next article I’ll cover mocks and stubs for a cleaner way. Our tests are back to the familiar – a setting object should be assigned, and we should expect an HTTP response of success. Our create wasn’t successful, but that’s not what we’re testing here. We’re testing the HTTP response, and success indicates there wasn’t an error at the HTTP level. Next, we’re rendering the new template again, for the user’s second try. We’re not setting the flash though, because you’re probably going to list off the individual errors anyway.
Edit
context "getting edit" do
setup do
get :edit, :id => @setting.id
end
should_assign_to(:setting){@setting}
should_respond_with :success
should_render_with_layout
should_render_template :edit
should_not_set_the_flash
end
This is back to the short-and-sweet familiar. This is almost like our new action, with a couple exceptions. First, we can now specify (by passing a block to the should_assign_to macro) the specific setting we’re expecting, because we specified it in our request. Also, we’re expecting the edit template to be rendered.
Update
context "putting update" do
context "with valid data" do
setup do
put :update, :id => @setting.id, :setting => {:name => 'Slappy'}
end
should_assign_to(:setting){@setting}
should_redirect_to("index page"){settings_path}
should_set_the_flash_to 'Your knowledge base was successfully updated.'
should "update the record"
assert Setting.find_by_name('Slappy')
end
end
context "with invalid data" do
setup do
put :update, :id => @setting.id, :setting => {:name => ""}
end
should_assign_to(:setting){@setting}
should_respond_with :success
should_render_with_layout
should_render_template :edit
should_not_set_the_flash
end
end
You can see we’re doing the same things here that we did with our create action. When calling update however, we can pass only the attributes we want to change. I check that it *has* changed, too.
We have to pass a hash with a piece of bad data to get a failure, and in this case just assume that name is an attribute that can’t be blank.
Destroy
context "deleting" do
setup do
delete :destroy, :id => @setting.id
end
should_assign_to(:setting){@setting}
should_redirect_to("index page"){settings_path}
should_set_the_flash_to "Your knowledge base was successfully deleted."
should "delete the record" do
assert !Setting.find_by_id(@setting.id)
end
This is pretty self-explanatory. We’re assigning, redirecting, and spitting out a nice flash message. We’re also doing a hard check to make sure the setting object is gone.
Members
From here, the tests get a lot easier to absorb. I’ll list the whole member context here:
context "as a member" do
setup do
login_as :quentin
end
context "attempting to get index" do
setup do
get :index
end
should_not_assign_to :settings
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
context "attempting to get new" do
setup do
get :new
end
should_not_assign_to :setting
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
context "attempting to create" do
setup do
post :create, :setting => {}
end
should_not_assign_to :setting
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
context "attempting to get edit" do
setup do
get :edit, :id => 1
end
should_not_assign_to :setting
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
context "attempting to update" do
setup do
put :update, :id => 1, :setting => {}
end
should_not_assign_to :setting
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
context "attempting to delete" do
setup do
delete :destroy, :id => 1
end
should_not_assign_to :setting
should_respond_with 401
should_render_with_layout false
should_not_set_the_flash
end
end
The only setup I need is to login as a regular user, in this case the default “quentin” that is created by restful_authentication. Notice every action has basically the same four tests: nothing is assigned, a 401 (unauthorized) HTTP error is returned, no layout is rendered, and no flash is set. In my action calls, I can use fake id numbers and empty attribute hashes, because they’ll never be checked anyway. So, no need to create an actual setting object.
By default, role_requirement shows a blank page, but I changed that to display a special 401.html file I created. This is a person who is already logged in, so we know who they are (authentication) but does not have permission to be where they are (authorization).
Visitors
context "as a visitor" do
context "attempting to get index" do
setup do
get :index
end
should_not_assign_to :settings
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
context "attempting to get new" do
setup do
get :new
end
should_not_assign_to :setting
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
context "attempting to create" do
setup do
post :create, :setting => {}
end
should_not_assign_to :setting
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
context "attempting to get edit" do
setup do
get :edit, :id => 1
end
should_not_assign_to :setting
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
context "attempting to update" do
setup do
put :update, :id => 1, :setting => {}
end
should_not_assign_to :setting
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
context "attempting to delete" do
setup do
delete :destroy, :id => 1
end
should_not_assign_to :setting
should_redirect_to("login"){new_session_path}
should_set_the_flash_to "You need to login to do this."
end
end
Visitors are different from members without access. We want to be nicer and assume visitors just aren’t logged in yet, if they try to access protected actions. So we simply redirect them to the login page with a nice message. Just as with members, there’s no need to create actual objects in the database, or give actual id numbers or attribute hashes, since the action will never get far enough to check.
Conclusion
Once I developed this basic set of tests, I was pleased with how consistent my controller coding became. I don’t have a generator for these, I code them by hand every time. It keeps me sharp, and doesn’t take that long when you get into the swing of it. This isn’t the final draft, however. I’ve since modified this template with mocking and stubbing to greatly improve the speed, cutting runtime to a quarter of what it was. I’ll write about that approach in my next article.
Files
Here are Pastie links to my sample controller test and controller files. My controller is mostly vanilla scaffold with all the format junk stripped out, and a couple of key protected methods at the end.
settings_controller_test.rb
settings_controller.rb
Tags: context, developers, functional, resources, restful, Ruby, ruby on rails, setup, shoulda, Testing, woulda
January 20, 2010 at 8:40 am |
[...] The Basics [...]
August 6, 2010 at 8:33 am |
applause, thanks a lot