Succinct specs for Rails named routes
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.
Leave a comment