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 Reply