├── .codeclimate.yml ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── rspec-jumpstart ├── lib ├── rspec_jumpstart.rb └── rspec_jumpstart │ ├── config.rb │ ├── erb_factory.rb │ ├── erb_templates.rb │ ├── generator.rb │ ├── rdoc_factory.rb │ └── version.rb ├── rspec-jumpstart ├── rspec-jumpstart.gemspec ├── samples ├── delta_template.erb └── full_template.erb └── spec ├── rspec_jumpstart ├── config_spec.rb ├── generator_spec.rb ├── rdoc_factory_spec.rb └── version_spec.rb ├── rspec_jumpstart_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | plugins: 4 | rubocop: 5 | enabled: true 6 | #checks: 7 | # Rubocop/Metrics/ClassLength: 8 | # enabled: false 9 | duplication: 10 | enabled: true 11 | config: 12 | languages: 13 | - ruby 14 | exclude_patterns: 15 | - config/application.rb 16 | - vendor/**/* 17 | - log/**/* 18 | - tmp/**/* 19 | - test/**/* 20 | - factories/**/* 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .idea 4 | .bundle 5 | .config 6 | .yardoc 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | AllCops: 4 | # Include gemspec and Rakefile 5 | TargetRubyVersion: 2.4 6 | Include: 7 | - '**/*.rb' 8 | Exclude: 9 | - 'bin/**/*' 10 | - 'tmp/**/*' 11 | - 'vendor/**/*' 12 | - 'Guardfile' 13 | 14 | AsciiComments: 15 | Enabled: false 16 | 17 | Metrics/ClassLength: 18 | Max: 200 19 | 20 | Metrics/CyclomaticComplexity: 21 | Max: 8 22 | 23 | Metrics/LineLength: 24 | Max: 120 25 | 26 | Metrics/MethodLength: 27 | Max: 80 28 | 29 | Style/AsciiComments: 30 | Enabled: false 31 | 32 | # Respect RubyMine formatter 33 | Layout/CaseIndentation: 34 | Enabled: false 35 | 36 | Style/Documentation: 37 | Enabled: false 38 | 39 | Layout/DotPosition: 40 | EnforcedStyle: trailing 41 | 42 | Style/GuardClause: 43 | Enabled: false 44 | 45 | Style/IfUnlessModifier: 46 | Enabled: false 47 | 48 | # Name reduce block params |a, e|. 49 | Style/SingleLineBlockParams: 50 | Enabled: false 51 | 52 | # Respect RubyMine formatter 53 | Layout/SpaceInsideHashLiteralBraces: 54 | Enabled: false 55 | 56 | # Cannot deal with AR#eager_load 57 | Style/SymbolProc: 58 | Enabled: false 59 | 60 | # Final newline missing. 61 | Layout/TrailingBlankLines: 62 | Enabled: false 63 | 64 | TrivialAccessors: 65 | Enabled: true 66 | ExactNameMatch: true 67 | 68 | Layout/SpaceInsideBlockBraces: 69 | Enabled: false 70 | 71 | Style/MultilineBlockChain: 72 | Enabled: false 73 | 74 | Style/NumericLiterals: 75 | Enabled: false 76 | 77 | Layout/AlignArray: 78 | Enabled: false 79 | 80 | # Do not prefix writer method names with set_ 81 | Layout/AccessorMethodName: 82 | Enabled: false 83 | 84 | Rails/Output: 85 | Enabled: false 86 | 87 | Layout/MultilineOperationIndentation: 88 | Enabled: false 89 | 90 | Layout/EmptyLinesAroundClassBody: 91 | Enabled: false 92 | 93 | Layout/EmptyLinesAroundBlockBody: 94 | Enabled: false 95 | 96 | Layout/EmptyLinesAroundModuleBody: 97 | Enabled: false 98 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.8 5 | - 2.2.4 6 | - 2.3.0 7 | script: bundle exec rspec spec 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rspec' 6 | gem 'rake' 7 | 8 | gem 'guard', require: false 9 | gem 'guard-rspec', require: false 10 | gem 'guard-rubocop', require: false 11 | gem 'rubocop-rspec', github: 'nevir/rubocop-rspec', require: false 12 | gem 'simplecov', require: false 13 | 14 | gemspec 15 | 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/nevir/rubocop-rspec.git 3 | revision: 4ab2e3f6654feb74fb6ccadf9403c64a2b50e8c1 4 | specs: 5 | rubocop-rspec (1.29.1) 6 | rubocop (>= 0.58.0) 7 | 8 | PATH 9 | remote: . 10 | specs: 11 | rspec-jumpstart (1.1.5) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | ast (2.4.0) 17 | coderay (1.1.2) 18 | diff-lcs (1.3) 19 | docile (1.1.5) 20 | ffi (1.9.25) 21 | ffi (1.9.25-java) 22 | formatador (0.2.5) 23 | guard (2.14.2) 24 | formatador (>= 0.2.4) 25 | listen (>= 2.7, < 4.0) 26 | lumberjack (>= 1.0.12, < 2.0) 27 | nenv (~> 0.1) 28 | notiffany (~> 0.0) 29 | pry (>= 0.9.12) 30 | shellany (~> 0.0) 31 | thor (>= 0.18.1) 32 | guard-compat (1.2.1) 33 | guard-rspec (4.7.3) 34 | guard (~> 2.1) 35 | guard-compat (~> 1.1) 36 | rspec (>= 2.99.0, < 4.0) 37 | guard-rubocop (1.3.0) 38 | guard (~> 2.0) 39 | rubocop (~> 0.20) 40 | jaro_winkler (1.5.1) 41 | jaro_winkler (1.5.1-java) 42 | json (2.3.1) 43 | json (2.3.1-java) 44 | listen (3.1.5) 45 | rb-fsevent (~> 0.9, >= 0.9.4) 46 | rb-inotify (~> 0.9, >= 0.9.7) 47 | ruby_dep (~> 1.2) 48 | lumberjack (1.0.13) 49 | method_source (0.9.0) 50 | nenv (0.3.0) 51 | notiffany (0.1.1) 52 | nenv (~> 0.1) 53 | shellany (~> 0.0) 54 | parallel (1.12.1) 55 | parser (2.5.1.2) 56 | ast (~> 2.4.0) 57 | powerpack (0.1.2) 58 | pry (0.11.3) 59 | coderay (~> 1.1.0) 60 | method_source (~> 0.9.0) 61 | pry (0.11.3-java) 62 | coderay (~> 1.1.0) 63 | method_source (~> 0.9.0) 64 | spoon (~> 0.0) 65 | rainbow (3.0.0) 66 | rake (13.0.1) 67 | rb-fsevent (0.10.3) 68 | rb-inotify (0.9.10) 69 | ffi (>= 0.5.0, < 2) 70 | rspec (3.7.0) 71 | rspec-core (~> 3.7.0) 72 | rspec-expectations (~> 3.7.0) 73 | rspec-mocks (~> 3.7.0) 74 | rspec-core (3.7.1) 75 | rspec-support (~> 3.7.0) 76 | rspec-expectations (3.7.0) 77 | diff-lcs (>= 1.2.0, < 2.0) 78 | rspec-support (~> 3.7.0) 79 | rspec-mocks (3.7.0) 80 | diff-lcs (>= 1.2.0, < 2.0) 81 | rspec-support (~> 3.7.0) 82 | rspec-support (3.7.0) 83 | rubocop (0.59.2) 84 | jaro_winkler (~> 1.5.1) 85 | parallel (~> 1.10) 86 | parser (>= 2.5, != 2.5.1.1) 87 | powerpack (~> 0.1) 88 | rainbow (>= 2.2.2, < 4.0) 89 | ruby-progressbar (~> 1.7) 90 | unicode-display_width (~> 1.0, >= 1.0.1) 91 | ruby-progressbar (1.10.0) 92 | ruby_dep (1.5.0) 93 | shellany (0.0.1) 94 | simplecov (0.15.1) 95 | docile (~> 1.1.0) 96 | json (>= 1.8, < 3) 97 | simplecov-html (~> 0.10.0) 98 | simplecov-html (0.10.2) 99 | spoon (0.0.6) 100 | ffi 101 | thor (0.20.0) 102 | unicode-display_width (1.4.0) 103 | 104 | PLATFORMS 105 | java 106 | ruby 107 | 108 | DEPENDENCIES 109 | guard 110 | guard-rspec 111 | guard-rubocop 112 | rake 113 | rspec 114 | rspec-jumpstart! 115 | rubocop-rspec! 116 | simplecov 117 | 118 | BUNDLED WITH 119 | 1.17.2 120 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec feature) 6 | 7 | ## Uncomment to clear the screen before every task 8 | # clearing :on 9 | 10 | ## Make Guard exit when config is changed so it can be restarted 11 | # 12 | ## Note: if you want Guard to automatically start up again, run guard in a 13 | ## shell loop, e.g.: 14 | # 15 | # $ while bundle exec guard; do echo "Restarting Guard..."; done 16 | # 17 | ## Note: if you are using the `directories` clause above and you are not 18 | ## watching the project directory ('.'), the you will want to move the Guardfile 19 | ## to a watched dir and symlink it back, e.g. 20 | # 21 | # $ mkdir config 22 | # $ mv Guardfile config/ 23 | # $ ln -s config/Guardfile . 24 | # 25 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 26 | # 27 | watch ("Guardfile") do 28 | UI.info "Exiting because Guard must be restarted for changes to take effect" 29 | exit 0 30 | end 31 | 32 | guard :rubocop do 33 | watch(%r{.+\.rb$}) 34 | watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } 35 | end 36 | 37 | # Note: The cmd option is now required due to the increasing number of ways 38 | # rspec may be run, below are examples of the most common uses. 39 | # * bundler: 'bundle exec rspec' 40 | # * bundler binstubs: 'bin/rspec' 41 | # * spring: 'bin/rspec' (This will use spring if running and you have 42 | # installed the spring binstubs per the docs) 43 | # * zeus: 'zeus rspec' (requires the server to be started separately) 44 | # * 'just' rspec: 'rspec' 45 | 46 | guard :rspec, cmd: "bundle exec rspec" do 47 | require "guard/rspec/dsl" 48 | dsl = Guard::RSpec::Dsl.new(self) 49 | 50 | # Feel free to open issues for suggestions and improvements 51 | 52 | # RSpec files 53 | rspec = dsl.rspec 54 | watch(rspec.spec_helper) { rspec.spec_dir } 55 | watch(rspec.spec_support) { rspec.spec_dir } 56 | watch(rspec.spec_files) 57 | 58 | # Ruby files 59 | ruby = dsl.ruby 60 | dsl.watch_spec_files_for(ruby.lib_files) 61 | 62 | # Rails files 63 | rails = dsl.rails(view_extensions: %w(erb haml slim)) 64 | dsl.watch_spec_files_for(rails.app_files) 65 | dsl.watch_spec_files_for(rails.views) 66 | 67 | watch(rails.controllers) do |m| 68 | [ 69 | rspec.spec.("routing/#{m[1]}_routing"), 70 | rspec.spec.("controllers/#{m[1]}_controller"), 71 | rspec.spec.("acceptance/#{m[1]}") 72 | ] 73 | end 74 | 75 | # Rails config changes 76 | watch(rails.spec_helper) { rspec.spec_dir } 77 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 78 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 79 | 80 | # Capybara features specs 81 | watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") } 82 | 83 | # Turnip features and steps 84 | watch(%r{^spec/acceptance/(.+)\.feature$}) 85 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 86 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - Kazuhiro Sera 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rspec-jumpstart 2 | 3 | RSpec 3 code generator toward existing Ruby code. This gem will help you working on existing legacy code which has no tests. 4 | 5 | [![Build Status](https://travis-ci.org/tjchambers/rspec-jumpstart.png)](https://travis-ci.org/tjchambers/rspec-jumpstart) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/f4a2577cd12ddb9c75ff/maintainability)](https://codeclimate.com/github/tjchambers/rspec-jumpstart/maintainability) 7 | 8 | ## Installation 9 | 10 | https://rubygems.org/gems/rspec-jumpstart 11 | 12 | gem install rspec-jumpstart 13 | 14 | ## Usage 15 | 16 | rspec-jumpstart ./app 17 | rspec-jumpstart ./lib 18 | rspec-jumpstart ./lib/yourapp/util.rb 19 | 20 | ## Options 21 | 22 | ``` 23 | $ rspec-jumpstart -h 24 | Usage: rspec-jumpstart [options] 25 | -f Create if absent or append to the existing spec 26 | --force 27 | -n Dry run mode (shows generated code to console) 28 | --dry-run 29 | -r Run in Rails mode 30 | --rails 31 | -o VAL Output directory (default: ./spec) 32 | --output-dir VAL 33 | -D VAL Delta template path (e.g. ./rails_controller_delta.erb) 34 | --delta-template VAL 35 | -F VAL Full template path (e.g. ./rails_controller_full.erb) 36 | --full-template VAL 37 | -v Print version 38 | --version 39 | ``` 40 | 41 | ## Output example 42 | 43 | Unfortunately, `lib/foo/bar_baz.rb` has no test. That's too bad... 44 | 45 | ```ruby 46 | module Foo 47 | class BarBaz 48 | 49 | def self.xxx(a, b = "aaa") 50 | end 51 | 52 | def yyy 53 | end 54 | 55 | private 56 | 57 | def zzz(a) 58 | end 59 | 60 | end 61 | end 62 | ``` 63 | 64 | OK, run `rspec-jumpstart` now! 65 | 66 | ```sh 67 | $ rspec-jumpstart lib/foo/bar_baz.rb 68 | ./spec/foo/bar_baz_spec.rb created. 69 | ``` 70 | 71 | `spec/foo/bar_baz_spec.rb` will be created as follows. 72 | 73 | ```ruby 74 | # -*- encoding: utf-8 -*- 75 | require 'spec_helper' 76 | require 'foo/bar_baz' 77 | 78 | describe Foo::BarBaz do 79 | 80 | # TODO: auto-generated 81 | describe '#xxx' do 82 | it 'works' do 83 | a = double('a') 84 | b = double('b') 85 | result = Foo::BarBaz.xxx(a, b) 86 | expect(result).not_to be_nil 87 | end 88 | end 89 | 90 | # TODO: auto-generated 91 | describe '#yyy' do 92 | it 'works' do 93 | bar_baz = Foo::BarBaz.new 94 | result = bar_baz.yyy 95 | expect(result).not_to be_nil 96 | end 97 | end 98 | 99 | end 100 | ``` 101 | 102 | ## Appending lacking test templates 103 | 104 | `-f` option allows appending lacking test templates to existing specs. 105 | 106 | For instance, `additional_ops` method is added after spec creation. 107 | 108 | ```ruby 109 | module Foo 110 | class BarBaz 111 | 112 | def self.xxx(a, b = "aaa") 113 | end 114 | 115 | def yyy 116 | end 117 | 118 | def additional_ops 119 | end 120 | 121 | private 122 | 123 | def zzz(a) 124 | end 125 | 126 | end 127 | end 128 | ``` 129 | 130 | Execute command. 131 | 132 | `rspec-jumpstart -f lib/foo/bar_baz.rb` 133 | 134 | The following code will be appended. 135 | 136 | ```ruby 137 | 138 | # TODO: auto-generated 139 | describe '#additional_ops' do 140 | it 'works' do 141 | bar_baz = Foo::BarBaz.new 142 | result = bar_baz.additional_ops 143 | expect(result).not_to be_nil 144 | end 145 | end 146 | 147 | ``` 148 | 149 | ## Rails mode 150 | 151 | In Rails mode, rspec-jumpstart generates Rails way spec code for controllers and helpers. 152 | 153 | ``` 154 | $ rspec-jumpstart -r app/controllers/root_controller.rb 155 | ``` 156 | 157 | Output for scaffold: 158 | 159 | ```ruby 160 | # -*- encoding: utf-8 -*- 161 | 162 | require 'rails_helper' 163 | 164 | describe CommentsController do 165 | 166 | # TODO: auto-generated 167 | describe 'GET index' do 168 | it 'works' do 169 | get :index, {}, {} 170 | expect(response.status).to eq(200) 171 | end 172 | end 173 | 174 | # TODO: auto-generated 175 | describe 'GET show' do 176 | it 'works' do 177 | get :show, {}, {} 178 | expect(response.status).to eq(200) 179 | end 180 | end 181 | 182 | # TODO: auto-generated 183 | describe 'GET new' do 184 | it 'works' do 185 | get :new, {}, {} 186 | expect(response.status).to eq(200) 187 | end 188 | end 189 | 190 | # TODO: auto-generated 191 | describe 'GET edit' do 192 | it 'works' do 193 | get :edit, {}, {} 194 | expect(response.status).to eq(200) 195 | end 196 | end 197 | 198 | # TODO: auto-generated 199 | describe 'POST create' do 200 | it 'works' do 201 | post :create, {}, {} 202 | expect(response.status).to eq(200) 203 | end 204 | end 205 | 206 | # TODO: auto-generated 207 | describe 'PUT update' do 208 | it 'works' do 209 | put :update, {}, {} 210 | expect(response.status).to eq(200) 211 | end 212 | end 213 | 214 | # TODO: auto-generated 215 | describe 'DELETE destroy' do 216 | it 'works' do 217 | delete :destroy, {}, {} 218 | expect(response.status).to eq(200) 219 | end 220 | end 221 | 222 | end 223 | ``` 224 | 225 | 226 | ## Customizable code template 227 | 228 | Try the template_samples. 229 | 230 | ``` 231 | ruby -Ilib bin/rspec-jumpstart --delta-template=samples/delta_template.erb --full-template=samples/full_template.erb lib/foo.rb -n 232 | ``` 233 | 234 | When you use customized templates for your apps, `gem install rspec-jumpstart` and do like this: 235 | 236 | ``` 237 | rspec-jumpstart lib -D misc/delta_template.erb -F misc/full_template.erb 238 | ``` 239 | 240 | ## Original work 241 | 242 | Many thanks to Kazuhiro Sera for rspec-kickstarter. 243 | 244 | ## RubyKaigi 2013 245 | 246 | Lightning talk about rspec-kickstarter at RubyKaigi 2013 247 | 248 | http://rubykaigi.org/2013/lightning_talks#seratch 249 | 250 | https://speakerdeck.com/seratch/a-test-code-generator-for-rspec-users 251 | 252 | ## Related Projects 253 | 254 | ### Vintage fork 255 | 256 | https://github.com/ifad/rspec-kickstarter-vintage 257 | 258 | This fork supports ruby 1.8.7 and RSpec 1.x with the old syntax. 259 | 260 | ### ToFactory 261 | 262 | https://github.com/markburns/to_factory 263 | 264 | ToFactory is a FactoryGirl's factories code generator for existing projects. 265 | 266 | ## License 267 | 268 | Copyright (c) 2018 - Tim Chambers 269 | 270 | MIT License 271 | 272 | https://github.com/hintmedia/rspec-jumpstart/blob/master/LICENSE.txt 273 | 274 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | -------------------------------------------------------------------------------- /bin/rspec-jumpstart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # rspec-jumpstart [dir/*.rb] 4 | # 5 | 6 | require 'optparse' 7 | require 'rspec_jumpstart/version' 8 | require 'rspec_jumpstart/generator' 9 | 10 | print_version = false 11 | force_write = false 12 | dry_run = false 13 | rails_mode = false 14 | spec_dir = './spec' 15 | delta_path = nil 16 | exc_spec_dir = nil 17 | full_path = nil 18 | 19 | opt = OptionParser.new 20 | opt.on('-f', 'Create if absent or append to the existing spec') { |_| force_write = true } 21 | opt.on('--force', '') { |_| force_write = true } 22 | 23 | opt.on('-n', 'Dry run mode (shows generated code to console)') { |_| dry_run = true } 24 | opt.on('--dry-run', '') { |_| dry_run = true } 25 | 26 | opt.on('-r', 'Run in Rails mode') { |_| rails_mode = true } 27 | opt.on('--rails', '') { |_| rails_mode = true } 28 | 29 | opt.on('-o VAL', 'Output directory (default: ./spec)') { |dir| spec_dir = dir } 30 | opt.on('--output-dir VAL', '') { |dir| spec_dir = dir } 31 | 32 | opt.on('-e VAL', 'Exclude directory (default: ./spec/no-run/**)') { |dir| exc_spec_dir = dir } 33 | opt.on('--exc-dir VAL', '') { |dir| exc_spec_dir = dir } 34 | 35 | opt.on('-D VAL', 'Delta template path (e.g. ./rails_controller_delta.erb)') { |path| delta_path = path } 36 | opt.on('--delta-template VAL', '') { |path| delta_path = path } 37 | 38 | opt.on('-F VAL', 'Full template path (e.g. ./rails_controller_full.erb)') { |path| full_path = path } 39 | opt.on('--full-template VAL', '') { |path| full_path = path } 40 | 41 | opt.on('-v', 'Print version') { |_| print_version = true } 42 | opt.on('--version', '') { |_| print_version = true } 43 | 44 | args = opt.parse(ARGV) 45 | 46 | if print_version 47 | puts "rspec-jumpstart #{RSpecJumpstart::VERSION}" 48 | exit 0 49 | end 50 | 51 | dir_or_file = args.first 52 | if dir_or_file.nil? 53 | puts "Usage: rspec-jumpstart [dir/*.rb] -options" 54 | exit 1 55 | end 56 | unless dir_or_file.match(/.rb$/) 57 | dir_or_file = dir_or_file.gsub(/\/$/, '') + "/**/*.rb" 58 | end 59 | 60 | delta_template = nil 61 | if delta_path 62 | if File.exist?(delta_path) 63 | delta_template = File.read(delta_path) 64 | else 65 | puts "'#{delta_path}' is not found." 66 | exit 1 67 | end 68 | end 69 | 70 | full_template = nil 71 | if full_path 72 | if File.exist?(full_path) 73 | full_template = File.read(full_path) 74 | else 75 | puts "'#{full_path}' is not found." 76 | exit 1 77 | end 78 | end 79 | 80 | generator = RSpecJumpstart::Generator.new(spec_dir, delta_template, full_template) 81 | 82 | files = Dir.glob(dir_or_file) 83 | files -= Dir.glob(exc_spec_dir) if exc_spec_dir 84 | 85 | files.each do |file_path| 86 | generator.write_spec(file_path, force_write, dry_run, rails_mode) 87 | end 88 | 89 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec_jumpstart/generator' 4 | require 'rspec_jumpstart/config' 5 | require 'rspec_jumpstart/version' 6 | 7 | # 8 | # RSpecJumpstart Facade 9 | # 10 | module RSpecJumpstart 11 | class << self 12 | # Returns RSpecJumpstart's configuration object. 13 | # @api private 14 | def config 15 | @config ||= RSpecJumpstart::Config.instance 16 | yield @config if block_given? 17 | @config 18 | end 19 | alias configure config 20 | 21 | def version 22 | VERSION 23 | end 24 | end 25 | 26 | def self.write_spec(file_path, spec_dir = './spec', force_write = false, dry_run = false) 27 | generator = RSpecJumpstart::Generator.new(spec_dir) 28 | generator.write_spec(file_path, force_write, dry_run) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | 5 | module RSpecJumpstart 6 | # Global configuration affecting all threads. 7 | class Config 8 | include Singleton 9 | 10 | attr_accessor :behaves_like_exclusions 11 | 12 | def initialize 13 | @behaves_like_exclusions = [] 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/erb_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'rspec_jumpstart' 5 | require 'rspec_jumpstart/erb_templates' 6 | 7 | # 8 | # ERB instance provider 9 | # 10 | module RSpecJumpstart 11 | class ERBFactory 12 | 13 | def initialize(custom_template) 14 | @custom_template = custom_template 15 | end 16 | 17 | # 18 | # Returns ERB instance for creating new spec 19 | # 20 | def get_instance_for_new_spec(rails_mode, target_path) 21 | template = get_erb_template(@custom_template, true, rails_mode, target_path) 22 | ERB.new(template, nil, '-', '_new_spec_code') 23 | end 24 | 25 | # 26 | # Returns ERB instance for appending lacking tests 27 | # 28 | def get_instance_for_appending(rails_mode, target_path) 29 | template = get_erb_template(@custom_template, false, rails_mode, target_path) 30 | ERB.new(template, nil, '-', '_additional_spec_code') 31 | end 32 | 33 | private 34 | 35 | # 36 | # Returns ERB template 37 | # 38 | def get_erb_template(custom_template, is_full, rails_mode, target_path) 39 | if custom_template 40 | custom_template 41 | elsif rails_mode && target_path.match(/controllers/) 42 | get_rails_controller_template(is_full) 43 | elsif rails_mode && target_path.match(/models/) 44 | get_rails_model_template(is_full) 45 | elsif rails_mode && target_path.match(/helpers/) 46 | get_rails_helper_template(is_full) 47 | else 48 | get_basic_template(is_full) 49 | end 50 | end 51 | 52 | def get_rails_controller_template(is_full) 53 | if is_full 54 | return RSpecJumpstart::ERBTemplates::RAILS_CONTROLLER_NEW_SPEC_TEMPLATE 55 | end 56 | 57 | RSpecJumpstart::ERBTemplates::RAILS_CONTROLLER_METHODS_PART_TEMPLATE 58 | end 59 | 60 | def get_rails_model_template(is_full) 61 | if is_full 62 | return RSpecJumpstart::ERBTemplates::RAILS_MODEL_NEW_SPEC_TEMPLATE 63 | end 64 | 65 | RSpecJumpstart::ERBTemplates::RAILS_MODEL_METHODS_PART_TEMPLATE 66 | end 67 | 68 | def get_rails_helper_template(is_full) 69 | if is_full 70 | return RSpecJumpstart::ERBTemplates::RAILS_HELPER_NEW_SPEC_TEMPLATE 71 | end 72 | 73 | RSpecJumpstart::ERBTemplates::RAILS_HELPER_METHODS_PART_TEMPLATE 74 | end 75 | 76 | def get_basic_template(is_full) 77 | if is_full 78 | return RSpecJumpstart::ERBTemplates::BASIC_NEW_SPEC_TEMPLATE 79 | end 80 | 81 | RSpecJumpstart::ERBTemplates::BASIC_METHODS_PART_TEMPLATE 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/erb_templates.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'rspec_jumpstart' 5 | 6 | # 7 | # ERB templates 8 | # 9 | module RSpecJumpstart 10 | module ERBTemplates 11 | 12 | BASIC_METHODS_PART_TEMPLATE = <<-SPEC 13 | <%- methods_to_generate.map { |method| %> 14 | # TODO: auto-generated 15 | describe '<%= decorated_name(method) %>' do 16 | it '<%= method.name %>' do 17 | <%- if get_instantiation_code(c, method) -%><%= get_instantiation_code(c, method) %><%- end -%> 18 | <%- if get_params_initialization_code(method) -%><%= get_params_initialization_code(method) %><%- end -%> 19 | result = <%= get_method_invocation_code(c, method).sub(c.to_s,'described_class') %> 20 | 21 | expect(result).not_to be_nil 22 | end 23 | end 24 | <% } %> 25 | SPEC 26 | 27 | BASIC_NEW_SPEC_TEMPLATE = <<-SPEC 28 | # frozen_string_literal: true 29 | 30 | <% if rails_mode then %>require 'rails_helper' 31 | <% else -%>require 'spec_helper' 32 | <% end -%> 33 | <% unless rails_mode then %>require '<%= self_path %>' 34 | <% end -%> 35 | 36 | RSpec.describe <%= to_string_namespaced_path_whole(file_path) %> do 37 | <%= ERB.new(BASIC_METHODS_PART_TEMPLATE, nil, '-').result(binding) -%> 38 | end 39 | SPEC 40 | 41 | RAILS_CONTROLLER_METHODS_PART_TEMPLATE = <<-SPEC 42 | <%- methods_to_generate.map { |method| %> 43 | # TODO: auto-generated 44 | describe '<%= decorated_name(method) %>' do 45 | it '<%= get_rails_http_method(method.name).upcase %> <%= method.name %>' do 46 | <%= get_rails_http_method(method.name) %> :<%= method.name %>, {}, {} 47 | 48 | expect(response).to have_http_status(:ok) 49 | end 50 | end 51 | <% } %> 52 | SPEC 53 | 54 | RAILS_CONTROLLER_NEW_SPEC_TEMPLATE = <<-SPEC 55 | # frozen_string_literal: true 56 | 57 | require 'rails_helper' 58 | 59 | RSpec.describe <%= (to_string_namespaced_path(self_path) + get_complete_class_name(c)).split('::').uniq.join('::') %>, type: :controller do 60 | <%= ERB.new(RAILS_CONTROLLER_METHODS_PART_TEMPLATE, nil, '-').result(binding) -%> 61 | end 62 | SPEC 63 | 64 | RAILS_MODEL_METHODS_PART_TEMPLATE = <<-SPEC 65 | <%= ERB.new(RAILS_MODEL_SCOPE_PART_TEMPLATE, nil, '-').result(binding) -%> 66 | <%- methods_to_generate.map { |method| %> 67 | # TODO: auto-generated 68 | describe '<%= decorated_name(method) %>' do 69 | it '<%= method.name %>' do 70 | <%- if get_instantiation_code(c, method) -%><%= get_instantiation_code(c, method) %><%- end -%> 71 | <%- if get_params_initialization_code(method) -%><%= get_params_initialization_code(method) %><%- end -%> 72 | result = <%= get_method_invocation_code(c, method).sub(c.to_s,'described_class') %> 73 | 74 | expect(result).not_to be_nil 75 | end 76 | end 77 | <% } %> 78 | SPEC 79 | 80 | RAILS_MODEL_SCOPE_PART_TEMPLATE = <<-SPEC 81 | <%- scope_methods_to_generate.map { |method| %> 82 | # TODO: auto-generated 83 | describe '.<%= method %>' do # scope test 84 | it 'supports named scope <%= method %>' do 85 | expect(described_class.limit(3).<%= method %>).to all(be_a(described_class)) 86 | end 87 | end<% } %> 88 | SPEC 89 | 90 | RAILS_MODEL_NEW_SPEC_TEMPLATE = <<-SPEC 91 | # frozen_string_literal: true 92 | 93 | require 'rails_helper' 94 | require 'shared_model_stuff' 95 | 96 | RSpec.describe <%= (to_string_namespaced_path(self_path) + get_complete_class_name(c)).split('::').uniq.join('::') %>, type: :model do 97 | it_behaves_like 'real_model' 98 | <%= ERB.new(RAILS_MODEL_METHODS_PART_TEMPLATE, nil, '-').result(binding) -%> 99 | end 100 | SPEC 101 | 102 | RAILS_HELPER_METHODS_PART_TEMPLATE = <<-SPEC 103 | <%- methods_to_generate.map { |method| %> 104 | # TODO: auto-generated 105 | describe '<%= decorated_name(method) %>' do 106 | it 'works' do 107 | result = <%= get_rails_helper_method_invocation_code(method) %> 108 | 109 | expect(result).not_to be_nil 110 | end 111 | end 112 | <% } %> 113 | SPEC 114 | 115 | RAILS_HELPER_NEW_SPEC_TEMPLATE = <<-SPEC 116 | # frozen_string_literal: true 117 | 118 | require 'rails_helper' 119 | 120 | RSpec.describe <%= (to_string_namespaced_path(self_path) + get_complete_class_name(c)).split('::').uniq.join('::') %>, type: :helper do 121 | <%= ERB.new(RAILS_HELPER_METHODS_PART_TEMPLATE, nil, '-').result(binding) -%> 122 | end 123 | SPEC 124 | 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rdoc' 4 | require 'rspec_jumpstart' 5 | require 'rspec_jumpstart/erb_factory' 6 | require 'rspec_jumpstart/erb_templates' 7 | require 'rspec_jumpstart/rdoc_factory' 8 | 9 | # 10 | # RSpec Code Generator 11 | # 12 | module RSpecJumpstart 13 | class Generator 14 | include RSpecJumpstart::ERBTemplates 15 | 16 | attr_accessor :spec_dir, :delta_template, :full_template 17 | 18 | def initialize(spec_dir = './spec', delta_template = nil, full_template = nil) 19 | @spec_dir = spec_dir.gsub(/\/$/, '') 20 | @delta_template = delta_template 21 | @full_template = full_template 22 | end 23 | 24 | # 25 | # Writes new spec or appends to the existing spec. 26 | # 27 | def write_spec(file_path, force_write = false, dry_run = false, rails_mode = false) 28 | begin 29 | code = '' 30 | class_or_module = RSpecJumpstart::RDocFactory.get_rdoc_class_or_module(file_path) 31 | if class_or_module 32 | spec_path = get_spec_path(file_path) 33 | 34 | code = if force_write && File.exist?(spec_path) 35 | append_to_existing_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) 36 | else 37 | create_new_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) 38 | end 39 | else 40 | puts red("#{file_path} skipped (Class/Module not found).") 41 | end 42 | rescue StandardError => e 43 | puts red("#{file_path} aborted - #{e.message}") 44 | end 45 | 46 | code 47 | end 48 | 49 | # 50 | # Gets the complete class name from RDoc::NormalClass/RDoc::NormalModule instance. 51 | # 52 | def get_complete_class_name(class_or_module, name = class_or_module.name) 53 | if class_or_module.parent.name && class_or_module.parent.is_a?(RDoc::NormalModule) 54 | get_complete_class_name(class_or_module.parent, "#{class_or_module.parent.name}::#{name}") 55 | else 56 | name 57 | end 58 | end 59 | 60 | def to_string_namespaced_path(self_path) 61 | path = self_path.split('/').map { |x| camelize(x) }[1..-2].uniq.join('::') 62 | path.empty? ? '' : path + '::' 63 | end 64 | 65 | def to_string_namespaced_path_whole(self_path) 66 | self_path. 67 | sub('.rb', ''). 68 | split('/'). 69 | map { |x| camelize(x) }[2..-1]. 70 | uniq. 71 | join('::') 72 | end 73 | 74 | def decorated_name(method) 75 | (method.singleton ? '.' : '#') + method.name 76 | end 77 | 78 | # 79 | # Returns spec file path. 80 | # e.g. "lib/foo/bar_baz.rb" -> "spec/foo/bar_baz_spec.rb" 81 | # 82 | def get_spec_path(file_path) 83 | spec_dir + '/' + 84 | file_path 85 | .gsub(/^\.\//, '') 86 | .gsub(%r{^(lib/)|(app/)}, '') 87 | .sub(/\.rb$/, '_spec.rb') 88 | end 89 | 90 | # 91 | # Returns string value to require. 92 | # e.g. "lib/foo/bar_baz.rb" -> "foo/bar_baz" 93 | # 94 | def to_string_value_to_require(file_path) 95 | file_path.gsub(%r{^(lib/)|(app/)}, '').gsub(/\.rb$/, '') 96 | end 97 | 98 | # 99 | # Returns snake_case name. 100 | # e.g. FooBar -> "foo_bar" 101 | # 102 | def instance_name(c) 103 | c.name. 104 | gsub(/::/, '/'). 105 | gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). 106 | gsub(/([a-z\d])([A-Z])/, '\1_\2'). 107 | tr('-', '_'). 108 | downcase 109 | end 110 | 111 | # 112 | # Extracts parameter names as an *Array*. 113 | # e.g. "()" -> [] 114 | # e.g. "(a, b = 'foo')" -> ["a", "b"] 115 | # 116 | def to_param_names_array(params) 117 | params. 118 | split(','). 119 | map { |p| p.gsub(/[()\s]/, '').gsub(/=.+$/, '') }. 120 | reject { |p| p.nil? || p.empty? } 121 | end 122 | 123 | # 124 | # Returns params part 125 | # e.g. ["a","b"] -> "(a, b)" 126 | # e.g. [] -> "" 127 | # 128 | def to_params_part(params) 129 | param_csv = to_param_names_array(params).join(', ') 130 | param_csv.empty? ? '' : "(#{param_csv})" 131 | end 132 | 133 | # 134 | # Creates new spec. 135 | # 136 | # rubocop:disable Metrics/AbcSize 137 | def create_new_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) 138 | # These names are used in ERB template, don't delete. 139 | methods_to_generate = public_methods_found(class_or_module) 140 | scope_methods_to_generate = scopes(class_or_module, file_path, spec_path) 141 | c = class_or_module 142 | self_path = to_string_value_to_require(file_path) 143 | # rubocop:enable Lint/UselessAssignment 144 | 145 | erb = RSpecJumpstart::ERBFactory. 146 | new(@full_template). 147 | get_instance_for_new_spec(rails_mode, file_path) 148 | code = erb.result(binding) 149 | 150 | if dry_run 151 | puts "----- #{spec_path} -----" 152 | puts code 153 | elsif File.exist?(spec_path) 154 | # puts yellow("#{spec_path} already exists.") 155 | else 156 | FileUtils.mkdir_p(File.dirname(spec_path)) 157 | File.open(spec_path, 'w') { |f| f.write(code) } 158 | puts green("#{spec_path} created.") 159 | end 160 | 161 | code 162 | end 163 | 164 | def public_methods_found(class_or_module) 165 | class_or_module.method_list.select do |m| 166 | m.visibility.equal?(:public) && m.name != 'new' 167 | end 168 | end 169 | 170 | # rubocop:enable Metrics/AbcSize 171 | 172 | # 173 | # Appends new tests to the existing spec. 174 | # 175 | # rubocop:disable Metrics/AbcSize 176 | def append_to_existing_spec(class_or_module, dry_run, rails_mode, file_path, spec_path) 177 | existing_spec = File.read(spec_path) 178 | if skip?(existing_spec) 179 | return 180 | end 181 | lacking_methods = public_methods_found(class_or_module). 182 | reject { |m| existing_spec.match(signature(m)) } 183 | 184 | scope_methods_to_generate = scopes(class_or_module, file_path, spec_path) 185 | if lacking_methods.empty? && scope_methods_to_generate.empty? 186 | # puts yellow("#{spec_path} skipped.") 187 | else 188 | # These names are used in ERB template, don't delete. 189 | methods_to_generate = lacking_methods 190 | c = class_or_module 191 | # rubocop:enable Lint/UselessAssignment 192 | 193 | erb = RSpecJumpstart::ERBFactory.new(@delta_template).get_instance_for_appending(rails_mode, spec_path) 194 | additional_spec = erb.result(binding).strip 195 | 196 | last_end_not_found = true 197 | code = existing_spec.split("\n").reverse.reject do |line| 198 | before_modified = last_end_not_found 199 | last_end_not_found = line.gsub(/#.+$/, '').strip != 'end' if before_modified 200 | before_modified 201 | end.reverse.join("\n") 202 | 203 | unless additional_spec.empty? 204 | code += "\n" + additional_spec + "\n" 205 | end 206 | 207 | code += "\nend\n" 208 | 209 | if dry_run 210 | puts "----- #{spec_path} -----" 211 | puts code 212 | else 213 | File.open(spec_path, 'w') { |f| f.write(code) } 214 | end 215 | puts green("#{spec_path} modified.") 216 | end 217 | 218 | code 219 | end 220 | 221 | def skip?(text) 222 | RSpecJumpstart.config.behaves_like_exclusions.each do |exclude_pattern| 223 | return true if text.match(exclude_pattern) 224 | end 225 | 226 | false 227 | end 228 | 229 | # rubocop:enable Metrics/AbcSize 230 | 231 | # ----- 232 | # Code generation 233 | # ----- 234 | 235 | # 236 | # e.g. 237 | # a = double('a') 238 | # b = double('b') 239 | # bar_baz = BarBaz.new(a, b) 240 | # 241 | def get_instantiation_code(c, method) 242 | return '' if method.singleton 243 | 244 | constructor = c.method_list.find { |m| m.name == 'new' } 245 | if constructor.nil? 246 | " #{instance_name(c)} = described_class.new\n" 247 | else 248 | get_params_initialization_code(constructor) + 249 | " #{instance_name(c)} = described_class.new#{to_params_part(constructor.params)}\n" 250 | end 251 | end 252 | 253 | # 254 | # e.g. 255 | # a = double('a') 256 | # b = double('b') 257 | # 258 | def get_params_initialization_code(method) 259 | code = to_param_names_array(method.params).map do |p| 260 | x = p.sub('*', '').sub('&', '') 261 | " #{x} = double('#{x}')" unless x.empty? 262 | end.compact.join("\n") 263 | code.empty? ? '' : "#{code}\n" 264 | end 265 | 266 | # 267 | # e.g. BarBaz.do_something(a, b) { |c| } 268 | # 269 | def get_method_invocation_code(c, method) 270 | target = method.singleton ? 'described_class' : instance_name(c) 271 | "#{target}.#{method.name}#{to_params_part(method.params)}#{get_block_code(method)}" 272 | end 273 | 274 | # 275 | # e.g. do_something(a, b) { |c| } 276 | # 277 | def get_rails_helper_method_invocation_code(method) 278 | "#{method.name}#{to_params_part(method.params)}#{get_block_code(method)}" 279 | end 280 | 281 | # 282 | # e.g. { |a, b| } 283 | # 284 | def get_block_code(method) 285 | return '' if method.block_params.nil? || method.block_params.empty? 286 | 287 | " { |#{method.block_params}| }" 288 | end 289 | 290 | def get_rails_http_method(method_name) 291 | RAILS_RESOURCE_METHOD_AND_HTTP_METHOD[method_name] || 'get' 292 | end 293 | 294 | private 295 | 296 | def parse_sexp(sexp, scopes, methods, stack = []) 297 | case sexp[0] 298 | when :module 299 | parse_sexp(sexp[2], scopes, methods, stack + [sexp[0], sexp[1][1][1]]) 300 | 301 | when :vcall 302 | name = sexp[1][1] 303 | return if name.eql?('private') 304 | 305 | when :command 306 | if sexp[1][0] == :@ident && sexp[1][1] == 'scope' 307 | name = sexp[2][1][0][1][1][1] 308 | scopes << name 309 | end 310 | 311 | when :class 312 | parse_sexp(sexp[3], scopes, methods, stack + [sexp[0], sexp[1][1][1]]) 313 | 314 | when :def 315 | name = sexp[1][1] 316 | # line_number = sexp[1][2][0] 317 | 318 | parse_sexp(sexp[3], scopes, methods, stack + [sexp[0], sexp[1][1]]) 319 | 320 | # puts "#{line_number}: Method: #{stack.last}##{name}\n" 321 | methods << name 322 | else 323 | if sexp.is_a?(Array) 324 | sexp.each { |s| parse_sexp(s, scopes, methods, stack) if s.is_a?(Array) } 325 | end 326 | end 327 | end 328 | 329 | require 'ripper' 330 | 331 | def scopes(_klass, file_path, spec_path) 332 | content = File.read(file_path) 333 | spec_content = ( 334 | begin 335 | File.read(spec_path) 336 | rescue 337 | '' 338 | end) 339 | sexp = Ripper.sexp(content) 340 | methods = [] 341 | scopes = [] 342 | 343 | parse_sexp(sexp, scopes, methods) 344 | 345 | scope_methods = [] 346 | scopes.each do |method| 347 | unless spec_content.include?("'.#{method}'") 348 | scope_methods << method 349 | end 350 | end 351 | 352 | scope_methods 353 | end 354 | 355 | def signature(method) 356 | "'#{decorated_name(method).sub('?', '\?').gsub('[', '\[').gsub(']', '\]')}'" 357 | end 358 | 359 | def colorize(text, color_code) 360 | "#{color_code}#{text}\033[0m" 361 | end 362 | 363 | def red(text) 364 | colorize(text, "\033[31m") 365 | end 366 | 367 | def green(text) 368 | colorize(text, "\033[32m") 369 | end 370 | 371 | def blue(text) 372 | colorize(text, "\033[34m") 373 | end 374 | 375 | def yellow(text) 376 | colorize(text, "\033[33m") 377 | end 378 | 379 | def camelize(str) 380 | str.split('_').map { |w| w.capitalize }.join 381 | end 382 | 383 | RAILS_RESOURCE_METHOD_AND_HTTP_METHOD = { 384 | 'index' => 'get', 385 | 'new' => 'get', 386 | 'create' => 'post', 387 | 'show' => 'get', 388 | 'edit' => 'get', 389 | 'update' => 'patch', 390 | 'destroy' => 'delete' 391 | }.freeze 392 | 393 | end 394 | end 395 | 396 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/rdoc_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rdoc' 4 | require 'rdoc/generator' 5 | require 'rdoc/options' 6 | require 'rdoc/parser/ruby' 7 | require 'rdoc/stats' 8 | require 'rspec_jumpstart' 9 | 10 | # 11 | # RDoc instance factory 12 | # 13 | module RSpecJumpstart 14 | class RDocFactory 15 | 16 | # 17 | # Returns RDoc::NormalClass/RDoc::NormalModule instance. 18 | # 19 | def self.get_rdoc_class_or_module(file_path) 20 | top_level = get_ruby_parser(file_path).scan 21 | extract_target_class_or_module(top_level) 22 | end 23 | 24 | # 25 | # Creates new RDoc::Parser::Ruby instance. 26 | # 27 | def self.get_ruby_parser(file_path) 28 | top_level = RDoc::TopLevel.new(file_path) 29 | 30 | # RDoc 4.0.0 requires RDoc::Store internally. 31 | store = RDoc::Store.new 32 | top_level.store = store 33 | stats = RDoc::Stats.new(store, 1) 34 | 35 | RDoc::Parser::Ruby.new( 36 | top_level, 37 | file_path, 38 | File.read(file_path), 39 | RDoc::Options.new, 40 | stats 41 | ) 42 | end 43 | 44 | # 45 | # Extracts RDoc::NormalClass/RDoc::NormalModule from RDoc::TopLevel. 46 | # 47 | def self.extract_target_class_or_module(top_level) 48 | c = top_level.classes.first 49 | return c if c 50 | 51 | m = top_level.modules.first 52 | if m.nil? 53 | top_level.is_a?(RDoc::NormalModule) ? top_level : nil 54 | else 55 | extract_target_class_or_module(m) 56 | end 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/rspec_jumpstart/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # Gem version 5 | # 6 | module RSpecJumpstart 7 | VERSION = '1.1.5' 8 | end 9 | -------------------------------------------------------------------------------- /rspec-jumpstart: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_dir=`dirname $0` 4 | cd ${current_dir} 5 | 6 | ruby -I ./lib ./bin/rspec-jumpstart $@ 7 | 8 | -------------------------------------------------------------------------------- /rspec-jumpstart.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'rspec_jumpstart/version' 6 | 7 | Gem::Specification.new do |gem| 8 | gem.name = 'rspec-jumpstart' 9 | gem.version = RSpecJumpstart::VERSION 10 | gem.authors = ['Timothy Chambers'] 11 | gem.email = ['tim@hint.io'] 12 | gem.licenses = ['MIT'] 13 | gem.description = 'rspec-jumpstart supports you writing tests for existing code.' 14 | gem.summary = 'rspec-jumpstart supports you writing tests for existing code.' 15 | gem.homepage = 'https://github.com/hintmedia/rspec-jumpstart' 16 | gem.files = Dir['{bin,lib}/**/*'] 17 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ['lib'] 20 | end 21 | -------------------------------------------------------------------------------- /samples/delta_template.erb: -------------------------------------------------------------------------------- 1 | <%- methods_to_generate.map { |method| %> 2 | # TODO auto-generated 3 | describe '#<%= method.name %>' do 4 | it 'works' do 5 | result = <%= get_rails_helper_method_invocation_code(method) %> 6 | 7 | expect(result).not_to be_nil 8 | end 9 | end 10 | <% } %> 11 | -------------------------------------------------------------------------------- /samples/full_template.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe <%= to_string_namespaced_path_whole(file_path) %> do 6 | <%= ERB.new(@delta_template, nil, '-').result(binding) -%> 7 | end 8 | 9 | -------------------------------------------------------------------------------- /spec/rspec_jumpstart/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rspec_jumpstart/config' 5 | 6 | module RSpecJumpstart 7 | ::RSpec.describe Config do 8 | describe '.instance' do 9 | it 'returns the singleton instance' do 10 | expect { described_class.instance }.not_to raise_error 11 | end 12 | end 13 | 14 | describe '.new' do 15 | it 'raises NoMethodError' do 16 | expect { described_class.new }.to raise_error(NoMethodError) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/rspec_jumpstart/generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rspec_jumpstart/generator' 5 | 6 | RSpec.describe RSpecJumpstart::Generator do 7 | 8 | let(:generator) { described_class.new('tmp/spec') } 9 | 10 | describe '#initialize' do 11 | it 'works without params' do 12 | result = described_class.new 13 | 14 | expect(result).not_to be_nil 15 | end 16 | it 'works' do 17 | spec_dir = './spec' 18 | 19 | result = described_class.new(spec_dir) 20 | 21 | expect(result).not_to be_nil 22 | end 23 | end 24 | 25 | describe '#get_complete_class_name' do 26 | it 'works' do 27 | parent = double(:parent, name: nil) 28 | c = double(:c, parent: parent) 29 | name = 'ClassName' 30 | 31 | result = generator.get_complete_class_name(c, name) 32 | 33 | expect(result).to eql('ClassName') 34 | end 35 | 36 | it 'works' do 37 | parent = double(:parent, name: 'A') 38 | c = double(:c, parent: parent) 39 | name = 'A::ClassName' 40 | 41 | result = generator.get_complete_class_name(c, name) 42 | 43 | expect(result).to eql('A::ClassName') 44 | end 45 | end 46 | 47 | describe '#instance_name' do 48 | it 'works' do 49 | c = double(:c, name: 'generator') 50 | 51 | result = generator.instance_name(c) 52 | 53 | expect(result).to eql('generator') 54 | end 55 | end 56 | 57 | describe '#to_param_names_array' do 58 | it 'works' do 59 | params = "(a, b = 'foo', c = 123)" 60 | 61 | result = generator.to_param_names_array(params) 62 | 63 | expect(result).to eql(%w[a b c]) 64 | end 65 | end 66 | 67 | describe '#get_params_initialization_code' do 68 | it 'works' do 69 | method = double(:method, params: "(a = 1,b = 'aaaa')") 70 | 71 | result = generator.get_params_initialization_code(method) 72 | expect(result).to eql(" a = double('a')\n b = double('b')\n") 73 | end 74 | end 75 | 76 | describe '#get_instantiation_code' do 77 | it 'works with modules' do 78 | method = double(:method, singleton: true, name: 'do_something') 79 | c = double(:c, name: 'Foo', method_list: [method]) 80 | 81 | result = generator.get_instantiation_code(c, method) 82 | 83 | expect(result).to eql('') 84 | end 85 | 86 | it 'works with classes' do 87 | parent = double(:parent, name: nil) 88 | method = double(:method, singleton: false, name: 'do_something') 89 | c = double(:c, name: 'Foo', parent: parent, method_list: [method]) 90 | 91 | result = generator.get_instantiation_code(c, method) 92 | 93 | expect(result).to eql(" foo = described_class.new\n") 94 | end 95 | 96 | it 'works with classes' do 97 | parent = double(:parent, name: 'Parent') 98 | method = double(:method, singleton: false, name: 'do_something') 99 | c = double(:c, name: 'Foo', parent: parent, method_list: [method]) 100 | 101 | result = generator.get_instantiation_code(c, method) 102 | 103 | expect(result).to eql(" foo = described_class.new\n") 104 | end 105 | end 106 | 107 | describe '#get_method_invocation_code' do 108 | it 'works with modules' do 109 | parent = double(:parent, name: nil) 110 | method = double(:method, 111 | singleton: true, 112 | name: 'do_something', 113 | params: '(a, b)', 114 | block_params: '') 115 | c = double(:c, name: 'Module', parent: parent, method_list: [method]) 116 | 117 | result = generator.get_method_invocation_code(c, method) 118 | expect(result).to eql('described_class.do_something(a, b)') 119 | end 120 | it 'works with classes' do 121 | parent = double(:parent, name: 'Module') 122 | method = double(:method, 123 | singleton: false, 124 | name: 'do_something', 125 | params: '(a, b)', 126 | block_params: '') 127 | c = double(:c, name: 'ClassName', parent: parent, method_list: [method]) 128 | 129 | result = generator.get_method_invocation_code(c, method) 130 | expect(result).to eql('class_name.do_something(a, b)') 131 | end 132 | end 133 | 134 | describe '#get_block_code' do 135 | it 'works with no arg' do 136 | method = double(:method, block_params: '') 137 | 138 | result = generator.get_block_code(method) 139 | expect(result).to eql('') 140 | end 141 | it 'works with 1 arg block' do 142 | method = double(:method, block_params: 'a') 143 | 144 | result = generator.get_block_code(method) 145 | expect(result).to eql(' { |a| }') 146 | end 147 | it 'works with 2 args block' do 148 | method = double(:method, block_params: 'a, b') 149 | 150 | result = generator.get_block_code(method) 151 | expect(result).to eql(' { |a, b| }') 152 | end 153 | end 154 | 155 | class CannotExtractTargetClass < RSpecJumpstart::Generator 156 | def extract_target_class_or_module(*) 157 | nil 158 | end 159 | end 160 | 161 | describe '#write_spec' do 162 | 163 | it 'just works' do 164 | file_path = 'lib/rspec_jumpstart.rb' 165 | generator.write_spec(file_path) 166 | end 167 | 168 | it 'works with -f option' do 169 | file_path = 'lib/rspec_jumpstart.rb' 170 | generator.write_spec(file_path, true) 171 | end 172 | 173 | it 'works with -n option' do 174 | file_path = 'lib/rspec_jumpstart.rb' 175 | generator.write_spec(file_path, false, true) 176 | end 177 | 178 | it 'works with no target class' do 179 | file_path = 'lib/rspec_jumpstart.rb' 180 | CannotExtractTargetClass.new.write_spec(file_path, true) 181 | end 182 | 183 | it 'creates new spec with full_template' do 184 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 185 | FileUtils.mkdir_p('tmp/spec') 186 | 187 | code = <<-CODE 188 | class Foo 189 | def hello; 'aaa'; end 190 | end 191 | CODE 192 | FileUtils.mkdir_p('tmp/lib') 193 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code) } 194 | 195 | generator.full_template = 'samples/full_template.erb' 196 | generator.write_spec('tmp/lib/foo.rb') 197 | end 198 | 199 | it 'appends new cases' do 200 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 201 | FileUtils.mkdir_p('tmp/spec') 202 | 203 | code = <<-CODE 204 | class Foo 205 | def hello; 'aaa'; end 206 | end 207 | CODE 208 | FileUtils.mkdir_p('tmp/lib') 209 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code) } 210 | 211 | generator.write_spec('tmp/lib/foo.rb') 212 | 213 | orig_size = File.size('tmp/spec/tmp/lib/foo_spec.rb') 214 | expect(orig_size).to be > 0 215 | 216 | code2 = <<-CODE 217 | class Foo 218 | def hello; 'aaa'; end 219 | def bye?; true; end 220 | end 221 | CODE 222 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code2) } 223 | generator.write_spec('tmp/lib/foo.rb', true, true) 224 | generator.write_spec('tmp/lib/foo.rb', true) 225 | 226 | new_size = File.size('tmp/spec/tmp/lib/foo_spec.rb') 227 | expect(new_size).to be > orig_size 228 | 229 | code2 = <<-CODE 230 | class Foo 231 | def initialize; end 232 | def hello; 'aaa'; end 233 | def bye?; true; end 234 | end 235 | CODE 236 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code2) } 237 | generator.write_spec('tmp/lib/foo.rb', true, true) 238 | generator.write_spec('tmp/lib/foo.rb', true) 239 | 240 | final_size = File.size('tmp/spec/tmp/lib/foo_spec.rb') 241 | expect(final_size).to equal new_size 242 | end 243 | 244 | it 'appends new cases with delta_template' do 245 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 246 | FileUtils.mkdir_p('tmp/spec') 247 | 248 | code = <<-CODE 249 | class Foo 250 | def hello; 'aaa'; end 251 | end 252 | CODE 253 | FileUtils.mkdir_p('tmp/lib') 254 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code) } 255 | 256 | generator.delta_template = 'sample/delta_template.erb' 257 | generator.write_spec('tmp/lib/foo.rb') 258 | 259 | code2 = <<-CODE 260 | class Foo 261 | def hello; 'aaa'; end 262 | def bye; 'aaa'; end 263 | end 264 | CODE 265 | File.open('tmp/lib/foo.rb', 'w') { |f| f.write(code2) } 266 | generator.write_spec('tmp/lib/foo.rb', true, true) 267 | generator.write_spec('tmp/lib/foo.rb', true) 268 | end 269 | 270 | it 'appends new cases with namespaced delta_template and namespaced' do 271 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 272 | FileUtils.rm_rf('tmp/lib') if File.exist?('tmp/lib') 273 | FileUtils.mkdir_p('tmp/spec') 274 | 275 | code = <<-CODE 276 | class Foo::Bar < Foo 277 | def hello; 'aaa'; end 278 | end 279 | CODE 280 | FileUtils.mkdir_p('tmp/lib/foo') 281 | File.open('tmp/lib/foo/bar.rb', 'w') { |f| f.write(code) } 282 | 283 | generator.delta_template = 'sample/delta_template.erb' 284 | generator.write_spec('tmp/lib/foo/bar.rb') 285 | 286 | code2 = <<-CODE 287 | class Foo::Bar < Foo 288 | def hello; 'aaa'; end 289 | def bye; 'aaa'; end 290 | end 291 | CODE 292 | 293 | FileUtils.rm_rf('tmp/lib') if File.exist?('tmp/lib') 294 | FileUtils.mkdir_p('tmp/lib/foo') 295 | File.open('tmp/lib/foo/bar.rb', 'w') { |f| f.write(code2) } 296 | generator.write_spec('tmp/lib/foo/bar.rb', true, true) 297 | generator.write_spec('tmp/lib/foo/bar.rb', true) 298 | end 299 | 300 | it 'works with rails models' do 301 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 302 | FileUtils.mkdir_p('tmp/spec') 303 | 304 | code = <<-CODE 305 | class FooModel 306 | scope :test_scope, -> { where(nil) } 307 | end 308 | CODE 309 | FileUtils.mkdir_p('tmp/app/models') 310 | File.open('tmp/app/models/foo_model.rb', 'w') { |f| f.write(code) } 311 | generator.write_spec('tmp/app/models/foo_model.rb', true, false, true) 312 | 313 | code = <<-CODE 314 | class FooModel 315 | scope :test_scope, -> { where(nil) } 316 | scope :test_scope1, -> { where(nil) } 317 | 318 | def foo 319 | end 320 | end 321 | CODE 322 | File.open('tmp/app/models/foo_model.rb', 'w') { |f| f.write(code) } 323 | puts generator.write_spec('tmp/app/models/foo_model.rb', true, false, true) 324 | end 325 | 326 | it 'works with rails controllers' do 327 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 328 | FileUtils.mkdir_p('tmp/spec') 329 | 330 | code = <<-CODE 331 | class FooController 332 | end 333 | CODE 334 | FileUtils.mkdir_p('tmp/app/controllers') 335 | File.open('tmp/app/controllers/foo_controller.rb', 'w') { |f| f.write(code) } 336 | generator.write_spec('tmp/app/controllers/foo_controller.rb', true, false, true) 337 | 338 | code = <<-CODE 339 | class FooController 340 | def foo 341 | end 342 | end 343 | CODE 344 | File.open('tmp/app/controllers/foo_controller.rb', 'w') { |f| f.write(code) } 345 | generator.write_spec('tmp/app/controllers/foo_controller.rb', true, false, true) 346 | end 347 | 348 | it 'works with rails helpers' do 349 | FileUtils.rm_rf('tmp/spec') if File.exist?('tmp/spec') 350 | FileUtils.mkdir_p('tmp/spec') 351 | 352 | code = <<-CODE 353 | class FooHelper 354 | end 355 | CODE 356 | FileUtils.mkdir_p('tmp/app/helpers') 357 | File.open('tmp/app/helpers/foo_helper.rb', 'w') { |f| f.write(code) } 358 | generator.write_spec('tmp/app/helpers/foo_helper.rb', true, false, true) 359 | 360 | code = <<-CODE 361 | class FooHelper 362 | def foo 363 | end 364 | end 365 | CODE 366 | File.open('tmp/app/helpers/foo_helper.rb', 'w') { |f| f.write(code) } 367 | generator.write_spec('tmp/app/helpers/foo_helper.rb', true, false, true) 368 | end 369 | 370 | end 371 | 372 | describe '#get_spec_path' do 373 | it 'works' do 374 | file_path = 'lib/foo/bar.rb' 375 | result = generator.get_spec_path(file_path) 376 | expect(result).to eql('tmp/spec/foo/bar_spec.rb') 377 | end 378 | it 'works with path which starts with current dir' do 379 | file_path = './lib/foo/bar.rb' 380 | result = generator.get_spec_path(file_path) 381 | expect(result).to eql('tmp/spec/foo/bar_spec.rb') 382 | end 383 | end 384 | 385 | describe '#to_string_value_to_require' do 386 | it 'works' do 387 | file_path = 'lib/foo/bar.rb' 388 | result = generator.to_string_value_to_require(file_path) 389 | expect(result).to eql('foo/bar') 390 | end 391 | end 392 | 393 | describe '#to_string_namespaced_path' do 394 | it 'works' do 395 | file_path = 'lib/foo/bar.rb' 396 | result = generator.to_string_namespaced_path(file_path) 397 | expect(result).to eql('Foo::') 398 | end 399 | 400 | it 'works' do 401 | file_path = 'lib/foo_baz/bar_bar/bar.rb' 402 | result = generator.to_string_namespaced_path(file_path) 403 | expect(result).to eql('FooBaz::BarBar::') 404 | end 405 | 406 | it 'works' do 407 | file_path = 'lib/foo/foo/bar.rb' 408 | result = generator.to_string_namespaced_path(file_path) 409 | expect(result).to eql('Foo::') 410 | end 411 | 412 | it 'works' do 413 | file_path = 'lib/bar.rb' 414 | result = generator.to_string_namespaced_path(file_path) 415 | expect(result).to eql('') 416 | end 417 | end 418 | 419 | describe '#get_rails_helper_method_invocation_code' do 420 | it 'works' do 421 | method = double(:method, 422 | singleton: false, 423 | name: 'do_something', 424 | params: '(a, b)', block_params: '') 425 | result = generator.get_rails_helper_method_invocation_code(method) 426 | 427 | expect(result).to eql('do_something(a, b)') 428 | end 429 | end 430 | 431 | describe '#get_rails_http_method' do 432 | it 'works' do 433 | expect(generator.get_rails_http_method('foo')).to eql('get') 434 | expect(generator.get_rails_http_method('index')).to eql('get') 435 | expect(generator.get_rails_http_method('new')).to eql('get') 436 | expect(generator.get_rails_http_method('create')).to eql('post') 437 | expect(generator.get_rails_http_method('show')).to eql('get') 438 | expect(generator.get_rails_http_method('edit')).to eql('get') 439 | expect(generator.get_rails_http_method('update')).to eql('patch') # RAILS 4.x+ 440 | expect(generator.get_rails_http_method('destroy')).to eql('delete') 441 | end 442 | end 443 | 444 | end 445 | -------------------------------------------------------------------------------- /spec/rspec_jumpstart/rdoc_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rspec_jumpstart/rdoc_factory' 5 | 6 | RSpec.describe RSpecJumpstart::RDocFactory do 7 | 8 | describe '#get_rdoc_class_or_module' do 9 | it 'works' do 10 | file_path = 'lib/rspec_jumpstart.rb' 11 | result = described_class.get_rdoc_class_or_module(file_path) 12 | expect(result).not_to be_nil 13 | end 14 | 15 | it 'works with Ruby 2.0' do 16 | unless defined?(RDoc::Store) 17 | module RDoc 18 | class Store 19 | end 20 | 21 | class TopLevel 22 | def store=(*); end 23 | end 24 | end 25 | 26 | begin 27 | file_path = 'lib/rspec_jumpstart.rb' 28 | result = described_class.get_rdoc_class_or_module(file_path) 29 | expect(result).not_to be_nil 30 | ensure 31 | RDoc.class_eval do 32 | remove_const :Store 33 | end 34 | end 35 | end 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/rspec_jumpstart/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'rspec_jumpstart/version' 5 | 6 | RSpec.describe RSpecJumpstart do 7 | 8 | describe RSpecJumpstart::VERSION do 9 | it 'exists' do 10 | expect(RSpecJumpstart::VERSION).not_to be_nil 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/rspec_jumpstart_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe RSpecJumpstart do 6 | 7 | describe '#write_spec' do 8 | it 'works' do 9 | file_path = 'lib/rspec_jumpstart.rb' 10 | spec_dir = './spec' 11 | force_write = false 12 | dry_run = false 13 | described_class.write_spec(file_path, spec_dir, force_write, dry_run) 14 | end 15 | end 16 | 17 | # TODO: auto-generated 18 | describe '.config' do 19 | it 'config' do 20 | result = described_class.config { |config| } 21 | 22 | expect(result).not_to be_nil 23 | end 24 | end 25 | 26 | # TODO: auto-generated 27 | describe '.configure' do 28 | it 'configure' do 29 | result = described_class.configure 30 | 31 | expect(result).not_to be_nil 32 | end 33 | end 34 | 35 | # TODO: auto-generated 36 | describe '.version' do 37 | it 'version' do 38 | result = described_class.version 39 | 40 | expect(result).not_to be_nil 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | if ENV['TRAVIS'] 6 | else 7 | SimpleCov.start 8 | end 9 | 10 | # RSpec.configure do |config| 11 | # end 12 | 13 | --------------------------------------------------------------------------------