Restful Controller Tests with Shoulda


View the Source Code

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:

  1. The Basics
  2. 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

About these ads

Tags: , , , , , , , , , ,

2 Responses to “Restful Controller Tests with Shoulda”

  1. Restful Controller Tests with Shoulda – Stubbing « Kansas City on Rails Says:

    [...] The Basics [...]

  2. Severin Says:

    applause, thanks a lot

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: