How should I test routes and controllers with rspec?

capybara, rspec, ruby-on-rails

Solution

Testing routes, especially standard RESTful routes, is not standard practice.

a) You don't want to waste effort retesting Rails' routing functionality

b) Your controller or request specs should fail when they cannot route a request

More often that not, writing and maintaining routing tests does not give much value and increased confidence. Consider testing routes when they become complex and error-prone.

That said, RSpec provides a `route_to` matcher for specifying that a request is routable.

The recommended place for your routing specs is under `spec/routing`, though it's not uncommon to see routing specs alongside controller specs. For example

describe VersionsController do
  describe 'routing' do
    it 'routes GET /version to VersionsController#show' do
      expect(get: '/version').to route_to(controller: 'versions', action: 'show')
    end
  end
end

The shoulda-matchers gem has its own route matcher, allowing you to write tests such as

describe PostsController do
  it { should route(:get, '/posts').to(action: :index) }
  it { should route(:get, '/posts/1').to(action: :show, id: 1) }
end

Problem

I have just one spec, located at `spec/controllers/statuses_spec.rb` Here is its contents: ``` require 'spec_helper' describe StatusesController do describe "routing" do it "routes to #index" do get("/statuses").should route_to("statuses#index") end end end ``` Suffice to say, I have a simple statuses scaffold, and the statuses controller has the standard actions for CRUD, including an index action. However, I get this failure when running the above test: ``` 15:39:52 - INFO - Running: ./spec/controllers/statuses_spec.rb:6 Run options: include {:locations=>{"./spec/controllers/statuses_spec.rb"=>[6]}} F Failures: 1) StatusesController routing routes to #index Failure/Error: get("/statuses").should route_to("statuses#index") ActionController::UrlGenerationError: No route matches {:controller=>"statuses", :action=>"/statuses"} # ./spec/controllers/statuses_spec.rb:8:in `block (3 levels) in <top (required)>' Finished in 0.21772 seconds 1 example, 1 failure ``` Rspec makes the assumption that I'm dealing with the `statuses` controller, which is sort of intuitive I guess because I referenced it in my spec's describe block, and it thinks the string I've passed into the get method ('/statuses') is the function. Frankly I don't really like this. I want to be able to test the exact string that is in the URL bar is going to the right controller#action pair. Regardless, I do as rspec says and do this: ``` require 'spec_helper' describe StatusesController do describe "routing" do it "routes to #index" do get("index").should route_to("statuses#index") end end end ``` However, now I get this: ``` Run options: include {:locations=>{"./spec/controllers/statuses_spec.rb"=>[6]}} F Failures: 1) StatusesController routing routes to #index Failure/Error: get("index").should route_to("statuses#index") NoMethodError: undefined method `values' for #<ActionController::TestResponse:0x00000102bd3208> # ./spec/controllers/statuses_spec.rb:8:in `block (3 levels) in <top (required)>' Finished in 0.31019 seconds 1 example, 1 failure Failed examples: rspec ./spec/controllers/statuses_spec.rb:6 # StatusesController routing routes to #index ``` I'm getting a no method error regarding a `values` method. Values? Seriously, just what? I have no idea why I'm getting this error. Here's my spec helper: ``` # This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'rspec/autorun' require 'capybara/rspec' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } # Checks for pending migrations before tests are run. # If you are not using ActiveRecord, you can remove this line. ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) RSpec.configure do |config| # ## Mock Framework # # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: # # config.mock_with :mocha # config.mock_with :flexmock # config.mock_with :rr config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:truncation) end config.before(:each) do Capybara.run_server = true Capybara.javascript_driver = :webkit Capybara.default_selector = :css Capybara.server_port = 7171 DatabaseCleaner.start end config.after(:each) do DatabaseCleaner.clean end # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" config.include RSpec::Rails::RequestExampleGroup, type: :feature # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true # If true, the base class of anonymous controllers will be inferred # automatically. This will be the default behavior in future versions of # rspec-rails. config.infer_base_class_for_anonymous_controllers = false # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = "random" end ```

Original source