Dave Schweisguth in a Bottle

How many meanings of that can you think of?

Succinct specs for Rails named routes

leave a comment »

I like all of my routes tested, and I like all of my routes named, and I like all of my named routes tested. I even like to test my RESTful routes; even though it feels a bit like testing Rails, it more than paid for itself when migrating to Rails 3. How, then, to spec a named route succinctly?

Let’s test this simple (non-RESTful) route (in routes.rb):

get 'about' => 'root#about', :as => root_about


There are two things about it to test: that the route connects to the correct controller and action, and that the name refers to the correct route. Stock RSpec tests the wiring like this:

describe RootController do
  describe '#about' do
    it "routes GET /about to root#about"
      { :get => '/about' }.should route_to :controller => 'root', :action => 'about'
    end
  end
end


Aside from the describe blocks (which we’re going to want in any case), that’s three lines of spec, which repeat both the URL and the route. Shoulda’s route matcher is much more succinct,

describe RootController do
  describe '#about' do
    it { should route(:get, '/about').to :action => 'about' }
  end
end


and it still produces a readable specification:

> rspec -f d spec/routing/root_routing_spec.rb
RootController
  #about
    should route GET /about to/from {:controller=>"root", :action=>"about"}


Now for the name. Stock RSpec would test a named route something like this:

describe RootController
  describe '#about'
    it "should have a route named root_about, where e.g. root_about_path == /about" do
      root_about_path.should == '/about'
    end
  end
end


which is three lines for each such spec, repeating the name twice and the URL once. That seems worth a custom matcher. In spec/support/matchers/routing.rb:

module MyApp
  module Matchers
    module Routing

      def have_named_route(name, *args)
        HaveNamedRoute.new(self, name, *args)
      end

      class HaveNamedRoute
        def initialize(context, name, *args)
          @context = context
          @name = name
          @path = "#{name}_path"
          @args = args
          if ! args.last
            raise ArgumentError, 'The last argument must be the expected uri'
          end
          @expected_uri = args.pop
        end

        def description
          "have a route named #{@name}, where e.g. #{example_call} == #{@expected_uri}"
        end

        def matches?(subject)
          @actual_uri = @context.send("#{@name}_path", *@args)
          @actual_uri == @expected_uri
        end

        def failure_message_for_should
          "expected #{example_call} to equal #{@expected_uri}, but got #{@actual_uri}"
        end

        def failure_message_for_should_not
          "expected #{example_call} to not equal #{@expected_uri}, but it did"
        end

        def example_call
          call = "#{@name}_path"
          if ! @args.empty?
            call << "(#{@args.map(&:to_s).join(', ')})"
          end
          call
        end

      end

    end
  end
end


and now it only takes one line to specify the name while still producing the same rspec documentation output as the spec written with stock RSpec:

describe RootController
  describe '#about'
    it { should have_named_route :root_about, '/about' }
  end
end


So the full specification of the route and its name, putting the specification of the name first for a more logical flow, is

describe RootController
  describe '#about'
    it { should have_named_route :root_about, '/about' }
    it { should route(:get, '/about').to :action => 'about' }
  end
end


Not too bad; only two lines, both readable, plus context. Hmm … next time around I might write a chained matcher to do it all in one line and eliminate a level of context, but this is doing the job for now.

Advertisements

Written by dschweisguth

May 11, 2011 at 08:48

Posted in Programming, Rails, Ruby, Testing

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