diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..616a6c1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in rspec-routes_coverage.gemspec +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07a81ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Andrew Shaydurov + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd4bca6 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Rspec::RoutesCoverage + +This gem is helpful on huge Rails JSON API applications: it allows to specify and track the coverage of tested API requests according to app's routes. + +## Installation + +Add this to your application's Gemfile: + + gem 'rspec-rails' + gem 'rspec-routes_coverage' + +And then execute: + + $ bundle + +## Usage + +spec/requests/items_spec.rb: + + require 'spec_helper' + describe ItemsController, request_path: '/items' do + describe_request :index, request_path: '/', method: 'GET' do + it 'lists items' do + perform_example_request # same as "get '/items/'" + # ... + end + end + + # another style: + describe_request 'GET /:id' do + it 'shows item' do + perform_example_request id: Item.first.id + # ... + end + end + end + +## TODO + +1. Make untested routes to be marked as pending specs +2. Gem tests :) +3. ????? + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f57ae68 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/lib/rspec-routes_coverage.rb b/lib/rspec-routes_coverage.rb new file mode 100644 index 0000000..570dd42 --- /dev/null +++ b/lib/rspec-routes_coverage.rb @@ -0,0 +1,56 @@ +require 'rspec/rails' +require 'rspec-routes_coverage/dsl' +require 'rspec-routes_coverage/helpers' + +module RSpec + module RoutesCoverage + class Railtie < ::Rails::Railtie + railtie_name :'rspec-routes_coverage' + + rake_tasks do + load "tasks/rspec-routes_coverage.rake" + end + end + + mattr_accessor :pending_routes + + def self.remove_pending_route(verb, path) + env = Rack::MockRequest.env_for path, method: verb.upcase + req = ::Rails.application.routes.request_class.new env + ::Rails.application.routes.router.recognize(req) do |route| + self.pending_routes.delete route + end + end + + def self.pending_routes? + initialize_routes! if ENV['WITH_ROUTES_COVERAGE'] && !@initialized + self.pending_routes.length > 0 + end + + def self.initialize_routes! + ::Rails.application.reload_routes! + self.pending_routes = ::Rails.application.routes.routes.routes.clone + @initialized = true + end + end +end + +if ENV['WITH_ROUTES_COVERAGE'] + RSpec.configure do |config| + config.after(:suite) do + inspector = begin + require 'rails/application/route_inspector' + Rails::Application::RouteInspector + rescue + require 'action_dispatch/routing/inspector' + ActionDispatch::Routing::RoutesInspector + end + + puts "\nPENDING ROUTES:\n" + inspector.new.format(RSpec::RoutesCoverage.pending_routes).each do |route| + puts route + end + puts "COVERED #{Rails.application.routes.routes.routes.length - RSpec::RoutesCoverage.pending_routes.length} OF #{Rails.application.routes.routes.routes.length} ROUTES" + end + end +end \ No newline at end of file diff --git a/lib/rspec-routes_coverage/dsl.rb b/lib/rspec-routes_coverage/dsl.rb new file mode 100644 index 0000000..eb2ad0c --- /dev/null +++ b/lib/rspec-routes_coverage/dsl.rb @@ -0,0 +1,28 @@ +module RSpec + module RoutesCoverage + module DSL + def describe_request(*args, &block) + unless args.last.is_a?(Hash) && args.last[:method] && args.last[:request_path] + verb, path = args[0].split ' ' + opts = { method: verb, request_path: path } + if args.last.is_a? Hash + args.last.merge! opts + else + args << opts + end + end + + describe *args do + before :all do + RSpec::RoutesCoverage.remove_pending_route get_example_request_verb, get_example_request_path + end if RSpec::RoutesCoverage.pending_routes? + + instance_eval(&block) if block + end + end + end + end +end + +extend RSpec::RoutesCoverage::DSL +Module.send(:include, RSpec::RoutesCoverage::DSL) \ No newline at end of file diff --git a/lib/rspec-routes_coverage/helpers.rb b/lib/rspec-routes_coverage/helpers.rb new file mode 100644 index 0000000..3c17b5d --- /dev/null +++ b/lib/rspec-routes_coverage/helpers.rb @@ -0,0 +1,34 @@ +require 'rspec/rails' +module RSpec + module RoutesCoverage + module Helpers + def perform_example_request(params=nil, headers=nil) + path = get_example_request_path + if params.is_a? Hash + params.each do |key, val| + path.gsub(":#{key}", val.to_s) if path.match ":#{key}" + end + end + self.send get_example_request_verb.downcase, path, params, headers + end + + def get_example_request_path + self.class.ancestors.reduce('') do |path, klass| + if klass.respond_to?(:metadata) && klass.metadata.is_a?(RSpec::Core::Metadata) && klass.metadata[:request_path] + klass.metadata[:request_path] + path + else + path + end + end + end + + def get_example_request_verb + self.class.metadata[:method] + end + end + end +end + +RSpec.configure do |config| + config.include RSpec::RoutesCoverage::Helpers +end \ No newline at end of file diff --git a/lib/rspec-routes_coverage/version.rb b/lib/rspec-routes_coverage/version.rb new file mode 100644 index 0000000..36f0e91 --- /dev/null +++ b/lib/rspec-routes_coverage/version.rb @@ -0,0 +1,5 @@ +module RSpec + module RoutesCoverage + VERSION = "0.0.1" + end +end diff --git a/lib/tasks/rspec-routes_coverage.rake b/lib/tasks/rspec-routes_coverage.rake new file mode 100644 index 0000000..f8680ff --- /dev/null +++ b/lib/tasks/rspec-routes_coverage.rake @@ -0,0 +1,10 @@ +namespace :spec do + namespace :requests do + desc 'run requests specs with counting of untested routes' + task :with_coverage do + files = Dir["spec/requests/**/*_spec.rb"].map{|f| "./"+f} + ENV["WITH_ROUTES_COVERAGE"] = 'true' + exec "ruby -S rspec #{files.join ' '}" + end + end +end \ No newline at end of file diff --git a/rspec-routes_coverage.gemspec b/rspec-routes_coverage.gemspec new file mode 100644 index 0000000..60f1443 --- /dev/null +++ b/rspec-routes_coverage.gemspec @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/rspec-routes_coverage/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Andrew Shaydurov"] + gem.email = ["a.shaydurov@roundlake.ru"] + gem.description = %q{TODO: Write a gem description} + gem.summary = %q{TODO: Write a gem summary} + gem.homepage = "" + + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "rspec-routes_coverage" + gem.require_paths = ["lib"] + gem.version = RSpec::RoutesCoverage::VERSION + + gem.add_dependency 'rspec-rails' + gem.add_dependency 'actionpack' +end