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":
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With