How should I test routes and controllers with rspec?

13,144

Solution 1

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

Solution 2

Routes should be done as part of integration tests. Integration tests are where you test the important work flows of your application - more specifically whether a URL is defined or not seems to be an important workflow.

Your integration test would look like any normal integration test:

require 'test_helper'
class RoutesTest < ActionController::IntegrationTest
   test "route test" do
       assert_generates "/videos/5", { :controller => "videos", :action => "show", :id => "1" }
       assert_generates "/about", :controller => "pages", :action => "about"
   end
end

As to @jemminger's response of not testing routes - While it is Rail's tests that verify that routes.rb works, it's not Rail's responsibility to test whether http://yoursite.com/users is defined in your routes. The caveat is that most route testing could be done in existing integration tests, so specific tests for routes could be redundant.

The specific use case I can think of are all the people that have already, or are going to upgrade from Rails 2 to Rails 3. The code to define routes has changed significantly, and it's better to find out from tests that the routes were upgraded correctly, than from users when they report 404 errors.

Share:
13,144
Starkers
Author by

Starkers

Updated on August 02, 2022

Comments

  • Starkers
    Starkers almost 2 years

    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