public
Last active

Write an Rspec test to assert that all your public API endpoints have matching rspec_api_documentation tests.

  • Download Gist
route_check.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
class RouteCheck
attr_reader :routes, :world
 
def initialize(routes: nil, world: nil)
@routes = routes
@world = world
end
 
def filtered_routes
collect_routes do |route|
next if route.internal?
next if route.verb.blank?
next if route.controller =~ /^devise.*/
next if route.controller =~ /^docs.*/
next if route.controller == "users"
next if route.controller == "authentication_jig"
next if route.controller == "invitations"
next if route.controller == "admin"
next if route.controller == "index"
route
end.compact
end
 
def api_specs
world.
example_groups.
map(&:descendants).
flatten.
reject{ |g| g.metadata.fetch(:api_doc_dsl, :not_found) == :not_found }.
reject{ |g| g.metadata.fetch(:method, :not_found) == :not_found }
end
 
def missing_docs
existing_routes = Set.new(matchable_routes(filtered_routes))
existing_route_specs = Set.new(matchable_specs(api_specs))
 
existing_routes - existing_route_specs
end
 
private
def matchable_routes(routes)
routes.collect do |r|
::Route.new(r.verb, r.path[/\/[^( ]+/])
end.compact
end
 
def matchable_specs(specs)
specs.map do |spec|
::Route.new(spec.metadata[:method], spec.metadata[:route])
end
end
 
def collect_routes
routes.collect do |route|
route = yield ActionDispatch::Routing::RouteWrapper.new(route)
end
end
end
 
class ::Route < Struct.new(:method, :path)
def eql? other
self.hash == other.hash
end
def == other
method.to_s.downcase == other.method.to_s.downcase and path.downcase == other.path.downcase
end
def hash
method.to_s.downcase.hash + path.downcase.hash
end
end
route_check_spec.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
require 'spec_helper'
require 'support/route_check'
require 'rspec_api_documentation/dsl'
 
describe RouteCheck do
let!(:application_routes) { ActionDispatch::Routing::RouteSet.new }
let!(:route) { route_builder('GET', 'projects/lebowski', 'projects#lebowski', application_routes) }
let!(:devise_route) { route_builder('POST', 'devise/users', 'users#bloop', application_routes)}
let!(:internal_route) { route_builder('GET', '/assets', 'rails#get_info', application_routes) }
 
 
describe '#filtered_routes' do
let(:routes) { RouteCheck.new(routes: application_routes.routes) }
let(:filtered_routes) { routes.filtered_routes }
 
it 'includes all non-filtered routes' do
filtered_routes.should == [route]
end
end
 
describe '#api_specs' do
subject(:route_check) { RouteCheck.new(world: world) }
let(:configuration) { RSpec::Core::Configuration.new }
let(:world) { RSpec::Core::World.new(configuration) }
 
describe 'returns routes for tests found in the world' do
context "when there are no examples" do
it "is empty" do
route_check.api_specs.should be_empty
end
end
 
context "when there are non-API examples" do
let(:regular_group) { RSpec::Core::ExampleGroup.describe("regular group") }
let(:action_context) { resource_group.context("/path/to/api", {:api_doc_dsl => :endpoint, :method => "METHOD", :route => "/path/to/api"}) }
let(:resource_group) do
RSpec::Core::ExampleGroup.describe("resource group", {:api_doc_dsl => :resource}) do
context "something different"
end
end
 
before do
world.register(regular_group)
world.register(resource_group)
action_context.register
end
 
it "returns only API test groups" do
route_check.api_specs.should == [action_context]
end
end
end
end
 
 
describe '#missing_docs' do
subject(:route_check) { RouteCheck.new(routes: application_routes.routes, world: world) }
 
let(:configuration) { RSpec::Core::Configuration.new }
let(:world) { RSpec::Core::World.new(configuration) }
let(:wrapped_route) { ActionDispatch::Routing::RouteWrapper.new(route) }
let(:formatted_route) { ::Route.new(wrapped_route.verb.downcase, wrapped_route.path[/\/[^( ]+/]) }
 
it 'detects routes for which no api test exists' do
route_check.missing_docs.should == [formatted_route].to_set
end
 
it 'does not return routes for which an api spec exists' do
group = RSpec::Core::ExampleGroup.describe("resource group", {:api_doc_dsl => :resource}) do
context("/path/to/api", {:api_doc_dsl => :endpoint, :method => :get, :route => '/projects/lebowski'})
end
world.register(group)
 
route_check.missing_docs.should be_empty
end
end
 
def route_builder(method, path, action, route_set)
scope = {:path_names=>{:new=>"new", :edit=>"edit"}}
path = path
name = path.split("/").last
options = {:via => method, :to => action, :anchor => true, :as => name}
mapping = ActionDispatch::Routing::Mapper::Mapping.new(route_set, scope, path, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
 
 
route_set.add_route(app, conditions, requirements, defaults, as, anchor)
end
end
route_spec.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12
require 'spec_helper'
require 'support/route_check'
 
# This test should go in the same folder with your API tests. It can only be run if the API tests are also running.
describe "APIs" do
it "tests all routes" do
Rails.application.reload_routes!
route_check = RouteCheck.new(routes: Rails.application.routes.routes, world: RSpec::world)
 
route_check.missing_docs.should == Set.new
end
end

I attempted to implement this based on the your blog post (http://pivotallabs.com/tested-tests-lately/). When I first run it, I get the following error.

NoMethodError: undefined method downcase' for nil:NilClass
./spec/support/route_check.rb:68:in
hash'
./spec/support/route_check.rb:34:in new'
./spec/support/route_check.rb:34:in
missing_docs'
./spec/controllers/api/v2/route_spec.rb:10:in `block (2 levels) in '

I implemented it pretty much verbatim to thise gist, even leaving the filtered_routes unchanged. I figured I could update them after seeing a successful run.

Not sure if this is a problem, but you suggested putting the route_spec.rb in the folder with API tests. We have a versioning system so my API tests are in /spec/controllters/api/V1 & /V2. I placed route_spec.rb in /spec/controllters/api and a few other places with the same results.

This is a fantastic idea for APIs, so I'd like to add this to our tests.

Any ideas?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.