Skip to content

Instantly share code, notes, and snippets.

@kayline
Last active August 29, 2015 13:56
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save kayline/8868438 to your computer and use it in GitHub Desktop.
Write an Rspec test to assert that all your public API endpoints have matching rspec_api_documentation tests.
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
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
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
@curtisolson
Copy link

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:inhash'
./spec/support/route_check.rb:34:in new' ./spec/support/route_check.rb:34:inmissing_docs'
./spec/controllers/api/v2/route_spec.rb:10:in `block (2 levels) in <top (required)>'

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