Testing Routes with RSpec
Testing routes can give you more confidence and help drive application development; here is how to do it with RSpec
Routing specs are one of the less-talked-about types of spec in RSpec’s toolbox. They offer you a speedy way to verify that specific URLs go to the correct controllers.
If you are using the standard RESTful routes that Rails defaults to, then the chances are you won’t get a tonne of lasting value from these tests; however, there are two situations where you will get lots of value;
- You are new to Rails, or someone on your team is, and the default routing is not clear
- You have complicated routes
You don’t get the default routing
When I first started using Rails, I was constantly confused by routing. What seemed basic in tutorials never seemed to stick in my head or translate well to my code.
I eventually “got” it, and now it feels like second nature.
You will get value from routing specs if it helps you drive your application forward. The “driving” part is often missed in Test Driven Development because it is about moving forward with confidence.
If adding one of these tests helps you understand things better and ultimately helps you move onto the next task, then the test adds value.
You have complicated routes
You might have complicated, deeply nested routes because your application is genuinely that complicated, or you might have inherited a codebase where lots of people have made inconsistent decisions along the way. Either way, a quick test can help with understanding and also stop regression.
Wouldn’t controller tests do this?
There are two reasons why I wouldn’t recommend using a controller test when you care about the routes.
- Routing specs are fast and have barely any setup
- Controller specs can get large at the best of times; adding routing tests into them will muddy the waters
How to use routing specs
There are two ways to tell RSpec you are dealing with a routing spec.
The first is to add type: :routing
to your main describe block, like so;
RSpec.describe "routes for Articles", type: :routing do
# tests go here
end
The second is if you have the setting config.infer_spec_type_from_file_location!
enabled for RSpec, then any file you place inside spec/routing
will be treated as a routing spec.
I prefer to be explicit and add type: :routing
to each spec; I find it easier than double-checking which directory a file is in.
route_to
The most common thing you want to do is check that a particular path goes to a specific controller action.
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "routes /articles to the articles controller" do
expect(get("/articles")).to route_to("articles#index")
end
end
Our test will check that when a user visits yoursite.com/articles
, Rails will hit the index action inside the ArticlesController.
If you had a namespaced route, you could test by adding the namespace to both sides of the test, here is how you could test the admin area;
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "routes admin/articles to the admin::articles controller" do
expect(get("admin/articles")).to route_to("admin/articles#index")
end
end
You can be more verbose about this, which is what we have to do when testing we have set a specific parameter; for example, if we wanted to see that visiting articles/my-article
sets params[:id]
to “my-article” we could do;
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "routes /articles/my-article to the articles controller" do
expect(get("/articles/my-article")).to route_to(
controller: 'articles',
action: 'show',
id: 'my-article'
)
end
end
be_routable
So far, we’ve tested that specific routes can go to their correct controller actions. Occasionally we need to do things like turn off routes or ensure that a route is not accessible. Situations like this are where be_routable
comes in useful.
You generally want to use this alongside the not_to
syntax to prove the negative.
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "does not route to articles" do
expect(get("/articles")).not_to be_routable
end
end
If Rails suddenly can route that endpoint to a controller action, the test will fail. These failures allow you to avoid accidentally turning something back on.
be_routable
is excellent at testing any constraints you have in your routes.rb file. If testing a full URL and not just a path as we have been doing, you should use the URL “test.host”.
If you have a constraint that limits one path to be https only, you could write;
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "allows https route for articles" do
expect(get("https://test.host/articles")).to be_routable
end
it "does not allow http route for articles" do
expect(get("http://test.host/articles")).not_to be_routable
end
end
Testing named routes
Rails gives us nice methods for talking about our routes. articles_path
will generate /articles
. If you’d like, you can test these methods instead of hard-coding the paths.
require "rails_helper"
RSpec.describe "routes for Articles", type: :routing do
it "routes articles_path to the articles controller" do
expect(get(articles_path)).to route_to("articles#index")
end
end
I don’t really like doing this for standard routes as it feels a bit like testing magic, but if it helps someone on your team better understand what is happening, then there is value in it for sure.
Different syntax
When you’re researching how to do routing tests, you will often come across code samples that use expect(get: '/articles)
instead of what I’ve used, expect(get('/articles'))
. They are identical, and it is a matter of personal preference. All of the HTTP verbs can be written as a hash or as methods.
The important thing is to be consistent.
1000 words on routing specs
When I wrote this article, I honestly thought it would be a couple of paragraphs long. It turns out I had more to see about this under-used spec than I thought! Hopefully you find it useful.