Write an Rspec test to assert that all your public API endpoints have matching rspec_api_documentation tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 <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?