Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test route constraints with rspec

I'm working on an application that will be primarily served as an API (other than a few minor views, such as session/registration, which will be "standard"). I like the approach that was finalized in Railscast #350: Versioning an API, and so followed it. My routes look like:

namespace :api, :defaults => {:format => 'json'} do
  scope :module => :v1, :constraints => ApiConstraints.new(:version => 1, :default => false) do
    resources :posts, :only => [:create, :show, :destroy, :index]
  end

  scope :module => :v2, :constraints => ApiConstraints.new(:version => 2, :default => true) do
    resources :posts, :only => [:create, :show, :destroy, :index]
  end
end

In each route, my Constraint is a new ApiConstraints object, which is located in my ./lib folder. The class looks like this:

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.MYAPP.v#{@version}")
  end
end

Now, when testing manually, everything works as expected. In my API, I may have between 5 and 10 controllers per version, and don't want to test that the API constraints works for each individual controller, as that makes no sense. I'm looking for one spec file that tests my API constraints, but I'm unsure of where to put that spec.

I've tried adding a spec/routing/api_spec.rb file to test things, but it's not working properly, as it complains that some things aren't provided, like so:

it "should route an unversioned request to the latest version" do
  expect(:get => "/api/posts", :format => "json").to route_to(:controller => "api/v1/posts")
end

The above throws an error even though the controller matches properly. It fails with the following error:

The recognized options <{"format"=>"json", "action"=>"index", "controller"=>"api/v1/posts"}>
did not match <{"controller"=>"api/v1/posts"}>,
difference: <{"format"=>"json", "action"=>"index"}>.

Notice that the controller was properly determined, but since I don't want to test for the format and action in this test, it errors out. I would like there to be 3 "API specs":

  • It should route an unversioned request to the latest version
  • It should default to the JSON format if none is specified
  • It should return a specified API version when requested

Does anyone have experience with writing specs for these kinds of routes? I don't want to add specs for every controller inside the API, as they're not responsible for this functionality.

like image 400
Mike Trpcic Avatar asked Apr 17 '13 18:04

Mike Trpcic


1 Answers

Rspec's route_to matcher delegates to ActionDispatch::Assertions::RoutingAssertions#assert_recognizes

The the argument to route_to is passed in as the expected_options hash (after some pre-processing that allows it to also understand shorthand-style arguments like items#index).

The the hash that you're expecting to match the route_to matcher (i.e., {:get => "/api/posts", :format => "json"}) is not actually a well-formed argument to expect. If you look at the source, you can see that we get the path to match against via

path, query = *verb_to_path_map.values.first.split('?')

The #first is a sure sign that we're expecting a hash with just one key-value pair. So the :format => "json" component is actually just being discarded, and isn't doing anything.

The ActionDispatch assertion expects you to be matching a complete path + verb to a complete set of controller, action, & path parameters. So the rspec matcher is just passing along the limitations of the method it delegates to.

It sounds like rspec's built-in route_to matcher won't do what you want it to. So the next suggestion would be to assume ActionDispatch will do what it is supposed to do, and instead just write specs for your ApiConstraints class.

To do that, I'd first recommend not using the default spec_helper. Corey Haines has a nice gist about how to make a faster spec helper that doesn't spin up the whole rails app. It may not be perfect for your case as-is, but I just thought I'd point it out since you're just instantiating basic ruby objects here and don't really need any rails magic. You could also try requiring ActionDispatch::Request & dependencies if you don't want to stub out the request object like I do here.

That would look something like

spec/lib/api_constraint.rb

require 'active_record_spec_helper'
require_relative '../../lib/api_constraint'

describe ApiConstraint do

  describe "#matches?" do

    let(:req) { Object.new }

     context "default version" do

       before :each do
         req.stub(:headers).and_return {}
         @opts = { :version => nil, :default => true }
       end

       it "returns true regardless of version number" do
         ApiConstraint.new(@opts).should match req
       end

     end

  end

end

...aaand I'll let you figure out exactly how to set up the context/write the expectations for your other tests.

like image 110
gregates Avatar answered Oct 23 '22 23:10

gregates