DRYing up your CRUD controller RSpecs

A lot of what we do in Rails boils down to simple Crud. If you're in the habit of developing admin sections to allow your clients to control the front end of their site, you'll probably have noticed that these controllers in particular tend to all look the same. There are quite a few ways to DRY up the controller itself - using something like make_resourceful, for example, but what about your RSpec files?

Robby Russell recently posted a short article about RSpec's Shared Example Groups. Take a look at his post or at the RSpec documentation (it's not scary) to see how they work in more detail, but they basically allow you to share your it "should" do ..... end blocks, enabling them to be reused multiple times.

When you think about it, every time we spec a basic CRUD controller, we're doing the same thing - we should be able to just do something like this:

require File.dirname(__FILE__) + '/../../spec_helper'

describe Admin::ContactsController do
before(:each) do
@model = 'Contact'
login_as_admin
end

it_should_behave_like "CRUD GET index"
it_should_behave_like "CRUD GET show"
it_should_behave_like "CRUD POST create"
it_should_behave_like "CRUD PUT update"
it_should_behave_like "CRUD DELETE destroy"
it_should_behave_like "CRUD GET edit"
end```

Well luckily for you we can! I've been using this pattern with most of my CRUD-based controllers for a while now. You just set up the model's name at the top (and in the case above perform the login_as_admin helper method to log the user in), and for each action in the controller use a specialised shared example group. The example groups all know how to understand your @model definition in before(:each) and map it to the various expectations that the CRUD specs run.

The great thing about this approach is that you can just dump these into your spec file and update them later if you need to. For example if you need to paginate the index action instead of the default find(:all) the shared example group will test, just remove the it_should_behave_like("CRUD GET index") and add your own describe block - the rest of the it_should_behave_like lines can stay as they are.

This approach works especially well if you take the approach to CRUD controllers where you create a CrudController class and subclass your CRUD controllers from it - e.g. from the example above:
```ruby
class Admin::ContactsController < Admin::CrudController

end
require File.dirname(__FILE__) + '/../../spec_helper'

describe Admin::ContactsController do
before(:each) do
@model = 'Contact'
login_as_admin
end

it_should_behave_like "CRUD GET index"
it_should_behave_like "CRUD GET show"
it_should_behave_like "CRUD POST create"
it_should_behave_like "CRUD PUT update"
it_should_behave_like "CRUD DELETE destroy"
it_should_behave_like "CRUD GET edit"
end```

Well luckily for you we can! I've been using this pattern with most of my CRUD-based controllers for a while now. You just set up the model's name at the top (and in the case above perform the login_as_admin helper method to log the user in), and for each action in the controller use a specialised shared example group. The example groups all know how to understand your @model definition in before(:each) and map it to the various expectations that the CRUD specs run.

The great thing about this approach is that you can just dump these into your spec file and update them later if you need to. For example if you need to paginate the index action instead of the default find(:all) the shared example group will test, just remove the it_should_behave_like("CRUD GET index") and add your own describe block - the rest of the it_should_behave_like lines can stay as they are.

This approach works especially well if you take the approach to CRUD controllers where you create a CrudController class and subclass your CRUD controllers from it - e.g. from the example above:
```ruby
class Admin::ContactsController < Admin::CrudController

end

My Admin::CrudController reflects on the name of the ContactsController subclass here and figures everything out without me having to do any work. This works because almost all of my Admin section code works the same way. If I need to diverge from the default CRUD behaviour, I just redefine the particular action in Admin::ContactsController:

class Admin::ContactsController < Admin::CrudController
def index
#my alternative implementation
end
end
class Admin::ContactsController < Admin::CrudController
def index
#my alternative implementation
end
end

I personally prefer this approach over the more declarative alternatives such as make_resourceful because I feel more in control this way. That said, I'm open to persuasion :)

Anyway the code for the shared example groups is on Github at http://github.com/edspencer/rspec-crud-controller-shared-example-groups. There's only one file there - just chuck in in your spec directory and add this line to spec_helper.rb:

require File.expand_path(File.dirname(__FILE__) + "/crud_controller_matchers")
require File.expand_path(File.dirname(__FILE__) + "/crud_controller_matchers")

Apart from the magic going on in the CrudSetup module at the top of that file, there's nothing special going on here, so you can tweak the examples to your particular approach. There's a good chance some of that code could be written more cleanly so please feel free to suggest changes / fork the file on Github.

UPDATE - Actually no, ignore the FUD about make_resourceful above, it works remarkably well with very few modifications to the shared groups - I'll post those up as soon as I'm done.

Share Post:

What to Read Next

On Leaving Sencha

As some of you may know, I left Sencha last week to move to another startup just up the road in San