Testing with Subdomain Routes
A lot of the queries I’ve had about the SubdomainRoutes gem relate to testing. There are two different areas where this comes into play – controller testing and testing of the routes themselves.
I’ll use a simple routes.rb as an example for this article:
map.subdomain :admin do |admin| admin.resources :users end map.subdomain :model => :city do |city| city.resources :reviews, :only => [ :index, :show ] end
(This connects a fixed admin subdomain to a Admin::UsersController, and a model-based city subdomain to a City::ReviewsController.)
Testing Controllers
A simple test for the Admin::UsersController#show action would go along these lines:
class Admin::UsersControllerTest < ActionController::TestCase test "show action" do get :show, :id => "1", :subdomains => [ "admin" ] assert_response :success # or whatever end end
Notice the :subdomains => [ "admin" ] in the hash passed to the get method. This is the additional requirement for testing controller actions which lie under a subdomain route. Your tests won’t work without it. The same applies for post, put and delete.
(For testing the model-based subdomain routes, :subdomains => :city_id and :city_id => "..." would be added to the route’s options hash. Check the specs for more examples, if you need them.)
It’s a little bit ugly (and not too DRY) to have to list the subdomains for the route in the test. Want to change the actual subdomains you are using? You’ll have to change your tests as well. But that’s the way it goes. (One way to avoid this brittleness, at least, would be to assign the subdomains to a constant and use the constant in your routes and tests.)
It’s easy to figure out what :subdomain option you should pass. Just look it up in your routes by typing rake routes at the console:
admin_users GET /users(.:format) {:action=>"index", :subdomains=>["admin"], :controller=>"admin/users"}
POST /users(.:format) {:action=>"create", :subdomains=>["admin"], :controller=>"admin/users"}
new_admin_user GET /users/new(.:format) {:action=>"new", :subdomains=>["admin"], :controller=>"admin/users"}
edit_admin_user GET /users/:id/edit(.:format) {:action=>"edit", :subdomains=>["admin"], :controller=>"admin/users"}
admin_user GET /users/:id(.:format) {:action=>"show", :subdomains=>["admin"], :controller=>"admin/users"}
PUT /users/:id(.:format) {:action=>"update", :subdomains=>["admin"], :controller=>"admin/users"}
DELETE /users/:id(.:format) {:action=>"destroy", :subdomains=>["admin"], :controller=>"admin/users"}
city_reviews GET /reviews(.:format) {:action=>"index", :subdomains=>:city_id, :controller=>"city/reviews"}
city_review GET /reviews/:id(.:format) {:action=>"show", :subdomains=>:city_id, :controller=>"city/reviews"}The :subdomain option you need to use is listed right there in the right-most column of each route.
Testing Subdomain Routes
Personally, I’m not really sure I see the point of testing routes. The DSL you use to map your URLs in the routes.rb file is pretty self-explanatory. Not only that, but the way Rails does route testing (assert_generates and assert_recognizes) is less than ideal, from what I can see. You’re required to pass the options for a route and see if it’s recognised. (I’d much rather just mock up a request with the host, path and method and test that that request gets resolved to the correct controller, action and parameters. A lot less abstract that way.)
In any event, people are obviously speccing their subdomain routes as I’ve had a few questions about how to do it. The short answer is that you can’t, at least not with the standard assertions that you get in Rails’ test suite.
An underlying assumption in the Rails routing code is that the path is all that’s needed to specify an URL, since the host is assumed to be fixed and irrelevant. In some parts of the routing assertions code, this assumption is fairly tightly entangled in the code. Obviously, for subdomain routes, it’s an invalid assumption.
I had a look at augmenting Rails’ assert_generates and assert_recognizes methods to allow for a changeable host, but it wasn’t really practical or sensible to do so. Instead, I’ve done the next best thing, which is to add some new assertions specifically for testing subdomain routes.
The caveat here is that I’m not experienced at testing routes, so I may have got one or two things wrong, or at least less-than-ideal. (As always, pull requests are welcome!) You’ll need to gem install the SubdomainRoutes gem again to get the newest version (0.3.0).
Testing Recognition
The signature for Rails’ traditional assert_recognizes method looks like this:
def assert_recognizes(expected_options, path, extras={}, message=nil)
The expected_options path is options hash describing the route that should be recognised (always including :controller and :action, as well as any other parameters that the route might produce). The path can either be a string representing the path, or a hash with :path and :method values (if you need to specify an HTTP method other than GET).
For assert_recognizes_with_host I’ve kept the same arguments, since the :host can be passed as another option in the path hash. The :host option represents what host should be set in the TestRequest that’s used to recognise the path. (Unlike traditional routes, the subdomain, and hence the host, is required for recognition of the route.)
So a typical passing recognition test for the user index route would be:
test "admin_users route recognition" do assert_recognizes_with_host( { :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] }, { :path => "/users", :host => "admin.example.com" }) end
Notice I’ve specified the correct subdomain for this route in the host. Note also the annoying :subdomains value in the first options hash. It needs to be there as well, to specify the route.
Testing Generation
Testing route generation is a little more involved. The Rails assertion is as follows:
def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
This method asserts that expected_path (a string) is the path generated by the route options. But with subdomain routes, the generated route may also depend on the current host – if the subdomain for the route is different than the current host, the host will be forced to the new subdomain.
To allow testing of this behaviour, I’ve introduced the assert_generates_with_host method. This assertion allows you to specify the current host, as well as the host that the new route should have (if different):
def assert_generates_with_host(expected_path, options, host, defaults={}, extras={}, message=nil)
Notice the additional third argument, host, which you should set to the current host (i.e. the host under which the route is being generated).
Now to test an example route. First, test the case where the host doesn’t change:
test "admin_users route generation for the same subdomain" do assert_generates_with_host( "/users", { :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] }, "admin.example.com") end
The assertion in this test is saying that, for admin.example.com, the index route should generate "/users" as the path, and not change the host. The test passes as this is expected behaviour.
The second test case covers the case of generating the route from a host with a different subdomain:
test "admin_users route generation for a different subdomain" do assert_generates_with_host( { :path => "/users", :host => "admin.example.com" }, { :controller => "admin/users", :action => "index", :subdomains => [ "admin" ] }, "www.example.com") end
Here the usage diverges from assert_generates: instead of passing a string as the expected path, a hash is passed. As with assert_recognizes, the hash is used to specify both the :path and the :host that the route should generate. The above test passes because the route changes the subdomain from www to admin. (This only occurs in a single-subdomain route, of course.)
Use with RSpec
The subdomain routing assertions won’t help you much if you’re using RSpec or some other testing framework. Your best bet is to wrap each assertion up in its own class, just as RSpec does with assert_recognizes in its route_for method (check the rspec-rails source code). This shouldn’t be too hard to do.
Conclusion
That’s about all for testing controllers and routes with the SubdomainRoutes gem. Controller testing is easy and just requires the addition of the :subdomains option in your call to get (or post, put or delete).
Testing the routes themselves is more involved and I’ve presented two new assertions that you can use to help you get the job done. But really – why bother? The SubdomainRoutes gem itself is extensively tested and should behave as claimed.