Hi folks,
I’ve been unhappy with routing specs for a long time now and last night
when updating some old 1.3 specs for 2.0 I decided to see if I could
come up with something that didn’t make me feel unhappy.
Principal causes of unhappiness:
- Historically we had “route_for” and “params_from”, which felt awfully
repetitive because we ended up doing:
route_for(lengthy_hash_of_params).should ==
string_or_hash_describing_destination
params_from(list_describing_destination).should ==
lengthy_hash_of_params
Of course, it was worse than that in practice because those two lines
usually appeared in separate example blocks with the associated
boilerplate. It felt like a lot of work for testing such a simple thing.
It also felt irritating to have to repeat basically the same thing twice
but in a different order.
- So then RSpec gave us “route_to”, which is a wrapper for Rails’
“assert_routing”; being a bi-directional test that encompasses the
function of both “assert_recognizes” and “assert_generates”, this allows
us to avoid some, or even all, of the repetition:
{ :get => ‘foo’ }.should route_to(:controller => ‘foo’, :action =>
‘index’)
The unhappiness here comes from three causes:
One is that { :get => ‘foo’ } feels inconsistent with other places in
RSpec like controller specs where “get” is a method, so we can do things
like “get ‘thing’”.
The second issue is that the “to” in “route_to” feels misleadingly
uni-directional when in reality it is a bi-directional test.
The third issue is that for routes which don’t actually have that
bi-directional mapping, “route_to” can’t be used and we must instead
drop down to Rails’ assert_recognizes() and/or assert_generates()
methods, or wrap them using our own matchers.
So I thought about what I would rather be writing and in my first cut
came up with this:
describe ArticlesController do
describe ‘routing’ do
example ‘GET /wiki’ do
get(‘/wiki’).should map_to(:controller => ‘articles’, :action
=> ‘index’)
get(‘/wiki’).should map_from(:controller => ‘articles’, :action
=> ‘index’)
articles_path.should == ‘/wiki’
end
end
end
Things to note:
-
make the bi-directionality of the mapping explicit by having separate
“map_to” and “map_from” lines. -
for ease of readability and writability, keep the order as “method →
path → destination” for both assertions by using “to” and “from”,
rather than swapping the order around -
“map” here is the right verb because we’ve always used that language
to talk about how a given URL “maps to” a given controller#action. And
remember how in the router DSL prior to Rails 3 everything in
config/routes.rb started with “map”? -
I’ve tacked a test for the “articles_path” URL helper in there,
because as a user of the Rails router I generally want to know two
things: firstly, that requests get mapped to the appropriate
controller#action; and secondly, that when I generate URLs (almost
exclusively with named helpers; I use “url_for” in only 4 places in my
entire app) that they take me where I think they take me. In the end,
however, I moved this into a separate “describe ‘URL helpers’” block. -
conscious use of “example” rather than “it” because I want my specs to
be identified as “ArticlesController routing GET /wiki” and not
“ArticlesController routing recognizes and generates #index”. -
the repetition is a conscious choice because I value
readability/scannability over DRYness-at-all-costs, especially in specs;
the following is more DRY, for example, but less readable/scannable:path = ‘/wiki’
destination = { :controller => 'articles, :action => ‘index’ }
get(path).should map_to(destination)
get(path).should map_from(destination)
So I went ahead and converted a bunch of specs to this syntax and found
that, surprise, surprise, in an application like this one where almost
everything consists of a “standard” RESTful resource, over 90% of routes
were testable in the bi-directional sense and in a typical routing spec
file I needed to use “map_to” with no corresponding “map_from” for only
one or two cases. So I needed a new method that meant “map_to_and_from”.
Funnily, I just can’t decide on a name for this method. As a placeholder
I am just using “map” for now:
get(‘/wiki’).should map(:controller => ‘articles’, :action => ‘index’)
But others I have tried are:
get(‘/wiki’).should map_as(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should map_via(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should map_with(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should map_to_and_from(:controller => ‘articles’, :action
=> ‘index’)
get(‘/wiki’).should map_both(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should map_both_ways(:controller => ‘articles’, :action
=> ‘index’)
get(‘/wiki’).should have_routing(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should have_route(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should be_route(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should be_routing(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should route_as(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should route_via(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should route(:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should <=> (:controller => ‘articles’, :action =>
‘index’)
get(‘/wiki’).should > (:controller => ‘articles’, :action => ‘index’)
map_to
get(‘/wiki’).should < (:controller => ‘articles’, :action => ‘index’)
map_from
If anybody has a suitable suggestion please let me know.
In the meantime, here is an example of a spec file that has been
converted to use this new “API”:
forums_routing_spec.rb · GitHub
It also includes the supporting code that adds these new “map”,
“map_to”, “map_from” matchers, and the “get”, “post”, “put” and “delete”
methods. All of this for Rails 3/RSpec 2 only.
I’m going to convert more routing specs and see if any more changes are
needed to handle edge cases, but for a first cut I am pretty happy with
the results, apart from my inability to decide on the right name for the
bi-directional “map” matcher.
Cheers,
Wincent