Last active

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

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

View route_check.rb
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
View route_check.rb
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
View route_check.rb
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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.