Scaffolding Nested Resources in Rails

Posted by paul
on 08 October 2008

Creating nested resources in Rails can be a little bit tedious, particularly if you're using Rspec and its scaffolding. Updating the controller isn't really a problem, but making the specs pass involves a bit too much busy work. And then there are the views to take care of, and their specs, and don't forget the routing specs while you're at it.

Now you might suggest that make_resourceful and resource_controller offer a neat solution to this problem. Well, yes, sometimes. Often though, I find these super controllers tend to be a little too DRY for my tastes, at least on some projects. I'm usually happier starting with some generated code and hacking from there.

So what I'd really like to be able to do is scaffold out the nested resource and its specs. A quick look around Github turned up rspec_on_rails_nested_scaffold, a plugin which does just that. It is very nearly perfect for my needs, although I have forked it and updated the templates used for views and specs with those from the latest versions of Rails and Rspec respectively.

You can install the plugin like so:

script/plugin install git://github.com/phorsfall/rspec_on_rails_nested_scaffold.git

If you already have a Post resource you can create nested Comments like this:

script/generate rspec_nested_scaffold Comment --owner=Post post_id:integer body:text

This will generate a CommentsController which includes a before_filter to load the correct Post and has Active Record calls scoped accordingly. You also get views which include the correct links and form actions, along with updated controller, view and routing specs. Before you are finished however, there are a few small things you'll need to do by hand:

  1. Update routes.rb. The generator will add an un-nested map.resources for our nested resource. In our example, we'd remove this and add the following:
    map.resources :posts, :has_many => :comments
  2. Create the Active Record associations in your models.
    class Post < ActiveRecord::Base
      has_many :comments
    end
    
    class Comments < ActiveRecord::Base
      belongs_to :post
    end
  3. If you didn't include a foreign key when you ran the generator, add one to the migration.

With that done, migrate the database and run the specs which should all pass. You now have an app you can go to work on and hopefully you've saved a bit of time along the way.

Thanks to Jeremy Friesen for putting the original plugin together.

An include_any Matcher for RSpec

Posted by paul
on 29 June 2008

Writing custom RSpec expectation matchers is a great way to enhance the readability of your specifications. As an example, here's a simple matcher to check whether a collection contains any of the given items. An example will make things clearer:

# Passing example
odds  = [1,3,5,7,9]
evens = [2,4,6,8]
odds.should_not include_any(*evens)

One thing to note is that include_any expects multiple arguments rather than a collection, hence the splat. Here's the code:

module Spec
  module Matchers
    def include_any(*args)
      IncludeAny.new(*args)
    end
  
    class IncludeAny
      def initialize(*args)
        @args = args
      end
    
      def matches?(target)
        @target = target
        @target.include_any?(*@args)
      end
    
      def failure_message
        "expected #{@target.inspect} to include one or more elements from #{@args.inspect}"
      end
    
      def negative_failure_message
        "expected #{@target.inspect} not to include any elements from #{@args.inspect}"
      end
    end
  end
end

This is about as trivial as a custom matcher gets. All of the work is done by the include_any? method, which doesn't actually exist as part of the core Ruby API. In fact it's a simple extension to Enumerable that I think I originally borrowed from Merb. Put the following in config/initializers/core_extensions.rb or similar to use it in a Rails app.

module Enumerable
  def include_any?(*args)
    args.any? { |arg| self.include?(arg) }
  end
end

Which custom matchers are you using?