rubocop-rspec-guide 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -6
  3. data/.yardopts +9 -0
  4. data/CHANGELOG.md +86 -0
  5. data/CONTRIBUTING.md +358 -0
  6. data/INTEGRATION_TESTING.md +324 -0
  7. data/README.md +443 -16
  8. data/Rakefile +49 -0
  9. data/benchmark/README.md +349 -0
  10. data/benchmark/baseline_v0.3.1.txt +67 -0
  11. data/benchmark/baseline_v0.4.0.txt +167 -0
  12. data/benchmark/benchmark_helper.rb +92 -0
  13. data/benchmark/compare_versions.rb +136 -0
  14. data/benchmark/cops_benchmark.rb +428 -0
  15. data/benchmark/cops_performance.rb +109 -0
  16. data/benchmark/quick_comparison.rb +58 -0
  17. data/benchmark/quick_invariant_bench.rb +52 -0
  18. data/benchmark/rspec_base_integration.rb +86 -0
  19. data/benchmark/save_baseline.rb +18 -0
  20. data/benchmark/scalability_benchmark.rb +181 -0
  21. data/config/default.yml +43 -2
  22. data/config/obsoletion.yml +6 -0
  23. data/lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb +193 -0
  24. data/lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb +10 -106
  25. data/lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb +13 -78
  26. data/lib/rubocop/cop/rspec_guide/context_setup.rb +81 -30
  27. data/lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb +89 -22
  28. data/lib/rubocop/cop/rspec_guide/duplicate_let_values.rb +89 -22
  29. data/lib/rubocop/cop/rspec_guide/happy_path_first.rb +52 -21
  30. data/lib/rubocop/cop/rspec_guide/invariant_examples.rb +60 -19
  31. data/lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb +165 -0
  32. data/lib/rubocop/rspec/guide/inject.rb +26 -0
  33. data/lib/rubocop/rspec/guide/plugin.rb +45 -0
  34. data/lib/rubocop/rspec/guide/version.rb +1 -1
  35. data/lib/rubocop-rspec-guide.rb +4 -0
  36. metadata +49 -1
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # RuboCop RSpec Guide
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/rubocop-rspec-guide.svg)](https://badge.fury.io/rb/rubocop-rspec-guide)
4
+ [![CI](https://github.com/rspec-guide/rubocop-rspec-guide/workflows/CI/badge.svg)](https://github.com/rspec-guide/rubocop-rspec-guide/actions)
5
+ [![Downloads](https://img.shields.io/gem/dt/rubocop-rspec-guide.svg)](https://rubygems.org/gems/rubocop-rspec-guide)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
7
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-blue.svg)](https://www.ruby-lang.org)
8
+
3
9
  Custom RuboCop cops that enforce best practices from the [RSpec Style Guide](https://github.com/AlexeyMatskevich/rspec-guide).
4
10
 
5
11
  ## Installation
@@ -18,28 +24,225 @@ gem install rubocop-rspec-guide
18
24
 
19
25
  ## Usage
20
26
 
21
- Add to your `.rubocop.yml`:
27
+ ### Quick Start
28
+
29
+ 1. **Add to Gemfile:**
30
+ ```ruby
31
+ group :development, :test do
32
+ gem 'rubocop-rspec-guide', require: false
33
+ end
34
+ ```
35
+
36
+ 2. **Install:**
37
+ ```bash
38
+ bundle install
39
+ ```
40
+
41
+ 3. **Configure `.rubocop.yml`:**
42
+
43
+ **Minimal configuration (v0.4.0+):**
44
+ ```yaml
45
+ # RuboCop 1.72+
46
+ plugins:
47
+ - rubocop-rspec-guide
48
+
49
+ # RuboCop < 1.72
50
+ require:
51
+ - rubocop-rspec-guide
52
+ ```
53
+
54
+ The gem automatically loads its default configuration, including RSpec Language settings.
55
+
56
+ **Optional - explicit config inheritance:**
57
+
58
+ If you want to explicitly inherit the config (not required):
59
+ ```yaml
60
+ plugins:
61
+ - rubocop-rspec-guide
62
+
63
+ inherit_gem:
64
+ rubocop-rspec-guide: config/default.yml
65
+ ```
66
+
67
+ 4. **Run RuboCop:**
68
+ ```bash
69
+ bundle exec rubocop
70
+ ```
71
+
72
+ 5. **Fix offenses automatically (where possible):**
73
+ ```bash
74
+ bundle exec rubocop -a
75
+ # or for safe autocorrection only:
76
+ bundle exec rubocop -A
77
+ ```
78
+
79
+ ### Autocorrection Support
80
+
81
+ Some cops support **automatic correction** with `rubocop -a`:
82
+
83
+ | Cop | Autocorrect | Safety |
84
+ |-----|-------------|--------|
85
+ | `FactoryBotGuide/DynamicAttributeEvaluation` | ✅ Yes | Safe |
86
+ | `RSpecGuide/MinimumBehavioralCoverage` | ❌ No | - |
87
+ | `RSpecGuide/HappyPathFirst` | ❌ No | - |
88
+ | `RSpecGuide/ContextSetup` | ❌ No | - |
89
+ | `RSpecGuide/DuplicateLetValues` | ❌ No | - |
90
+ | `RSpecGuide/DuplicateBeforeHooks` | ❌ No | - |
91
+ | `RSpecGuide/InvariantExamples` | ❌ No | - |
92
+
93
+ **Example of autocorrection:**
94
+
95
+ ```ruby
96
+ # Before: offense detected
97
+ factory :user do
98
+ created_at Time.now
99
+ token SecureRandom.hex
100
+ end
101
+
102
+ # After: rubocop -a
103
+ factory :user do
104
+ created_at { Time.now }
105
+ token { SecureRandom.hex }
106
+ end
107
+ ```
108
+
109
+ ### Common Patterns
110
+
111
+ #### Pattern 1: Testing Happy Path + Edge Cases
112
+
113
+ ```ruby
114
+ # Before (offense)
115
+ describe '#calculate_discount' do
116
+ it 'calculates discount' do
117
+ expect(calculate_discount(100)).to eq(10)
118
+ end
119
+ end
120
+
121
+ # After (fixed)
122
+ describe '#calculate_discount' do
123
+ context 'with standard price' do
124
+ it { expect(calculate_discount(100)).to eq(10) }
125
+ end
126
+
127
+ context 'with zero price' do
128
+ it { expect(calculate_discount(0)).to eq(0) }
129
+ end
130
+
131
+ context 'with negative price' do
132
+ it { expect { calculate_discount(-10) }.to raise_error(ArgumentError) }
133
+ end
134
+ end
135
+ ```
136
+
137
+ #### Pattern 2: It-blocks + Context-blocks
138
+
139
+ ```ruby
140
+ # Good - default behavior + edge cases
141
+ describe '#process_payment' do
142
+ it 'processes payment successfully' do
143
+ expect(process_payment).to be_success
144
+ end
145
+
146
+ context 'when payment gateway is down' do
147
+ before { stub_gateway_down }
148
+ it { expect(process_payment).to be_failure }
149
+ end
150
+
151
+ context 'with insufficient funds' do
152
+ let(:balance) { 0 }
153
+ it { expect(process_payment).to be_declined }
154
+ end
155
+ end
156
+ ```
157
+
158
+ #### Pattern 3: Extracting Duplicate Setup
159
+
160
+ ```ruby
161
+ # Before (offense - duplicate let in all contexts)
162
+ describe 'PaymentProcessor' do
163
+ context 'with credit card' do
164
+ let(:currency) { :usd }
165
+ it { expect(process).to be_success }
166
+ end
167
+
168
+ context 'with paypal' do
169
+ let(:currency) { :usd } # Duplicate!
170
+ it { expect(process).to be_success }
171
+ end
172
+ end
173
+
174
+ # After (fixed - extracted to parent)
175
+ describe 'PaymentProcessor' do
176
+ let(:currency) { :usd } # Moved to parent
177
+
178
+ context 'with credit card' do
179
+ it { expect(process).to be_success }
180
+ end
181
+
182
+ context 'with paypal' do
183
+ it { expect(process).to be_success }
184
+ end
185
+ end
186
+ ```
187
+
188
+ ### Configuration Examples
189
+
190
+ #### Complete Setup (with rubocop-rspec and rubocop-factory_bot)
191
+
192
+ Most projects use `rubocop-rspec-guide` alongside `rubocop-rspec` and `rubocop-factory_bot`. Here's a complete configuration:
22
193
 
23
194
  ```yaml
195
+ # .rubocop.yml
196
+
197
+ # Load all RSpec-related extensions
24
198
  require:
199
+ - rubocop-rspec
200
+ - rubocop-rspec_rails # If using Rails
201
+ - rubocop-factory_bot
25
202
  - rubocop-rspec-guide
26
203
 
27
- # Optionally inherit the default config
28
- inherit_gem:
29
- rubocop-rspec-guide: config/default.yml
204
+ # Or use plugins syntax (RuboCop 1.72+):
205
+ # plugins:
206
+ # - rubocop-rspec
207
+ # - rubocop-rspec_rails
208
+ # - rubocop-factory_bot
209
+ # - rubocop-rspec-guide
210
+
211
+ # RSpec cops (from rubocop-rspec)
212
+ RSpec/VerifiedDoubles:
213
+ Enabled: true
214
+
215
+ RSpec/MessageSpies:
216
+ Enabled: true
217
+ EnforcedStyle: have_received
218
+
219
+ # FactoryBot cops (from rubocop-factory_bot)
220
+ FactoryBot/CreateList:
221
+ Enabled: true
222
+
223
+ # RSpec Style Guide cops (from rubocop-rspec-guide)
224
+ RSpecGuide/MinimumBehavioralCoverage:
225
+ Enabled: true
226
+
227
+ RSpecGuide/HappyPathFirst:
228
+ Enabled: true
30
229
 
31
- # Recommended: Enable RSpec/LeadingSubject to ensure subject is at describe level
32
- RSpec/LeadingSubject:
230
+ RSpecGuide/ContextSetup:
231
+ Enabled: true
232
+
233
+ FactoryBotGuide/DynamicAttributeEvaluation:
33
234
  Enabled: true
34
235
  ```
35
236
 
36
- Or configure cops individually:
237
+ **Note:** The gem automatically injects RSpec Language configuration (v0.4.0+), so no `inherit_gem` is needed.
238
+
239
+ #### Strict Mode (for new projects)
37
240
 
38
241
  ```yaml
39
242
  require:
40
243
  - rubocop-rspec-guide
41
244
 
42
- RSpecGuide/CharacteristicsAndContexts:
245
+ RSpecGuide/MinimumBehavioralCoverage:
43
246
  Enabled: true
44
247
 
45
248
  RSpecGuide/HappyPathFirst:
@@ -50,23 +253,200 @@ RSpecGuide/ContextSetup:
50
253
 
51
254
  RSpecGuide/DuplicateLetValues:
52
255
  Enabled: true
256
+ WarnOnPartialDuplicates: true
53
257
 
54
258
  RSpecGuide/DuplicateBeforeHooks:
55
259
  Enabled: true
260
+ WarnOnPartialDuplicates: true
56
261
 
57
262
  RSpecGuide/InvariantExamples:
58
263
  Enabled: true
59
264
  MinLeafContexts: 3
60
265
 
266
+ FactoryBotGuide/DynamicAttributeEvaluation:
267
+ Enabled: true
268
+ ```
269
+
270
+ #### Relaxed Mode (for legacy projects)
271
+
272
+ ```yaml
273
+ require:
274
+ - rubocop-rspec-guide
275
+
276
+ # Enable only critical cops
277
+ RSpecGuide/MinimumBehavioralCoverage:
278
+ Enabled: true
279
+
280
+ RSpecGuide/ContextSetup:
281
+ Enabled: true
282
+
283
+ # Disable warnings for partial duplicates
284
+ RSpecGuide/DuplicateLetValues:
285
+ Enabled: true
286
+ WarnOnPartialDuplicates: false
287
+
288
+ RSpecGuide/DuplicateBeforeHooks:
289
+ Enabled: true
290
+ WarnOnPartialDuplicates: false
291
+
292
+ # More lenient threshold for invariants
293
+ RSpecGuide/InvariantExamples:
294
+ Enabled: true
295
+ MinLeafContexts: 5 # Only report if in 5+ contexts
296
+
297
+ # Disable strict cops
298
+ RSpecGuide/HappyPathFirst:
299
+ Enabled: false
300
+
301
+ FactoryBotGuide/DynamicAttributeEvaluation:
302
+ Enabled: true
303
+ ```
304
+
305
+ ### Troubleshooting
306
+
307
+ #### Issue: Too many offenses in existing codebase
308
+
309
+ **Solution:** Enable cops gradually:
310
+
311
+ ```yaml
312
+ # Start with most important cops
313
+ RSpecGuide/ContextSetup:
314
+ Enabled: true
315
+
316
+ FactoryBotGuide/DynamicAttributeEvaluation:
317
+ Enabled: true
318
+
319
+ # Disable others temporarily
320
+ RSpecGuide/MinimumBehavioralCoverage:
321
+ Enabled: false
322
+
323
+ RSpecGuide/DuplicateLetValues:
324
+ Enabled: false
325
+ ```
326
+
327
+ Then enable one cop at a time, fix offenses, and move to the next.
328
+
329
+ #### Issue: False positives on simple getters
330
+
331
+ **Solution:** Disable cop for specific tests:
332
+
333
+ ```ruby
334
+ describe '#name' do # rubocop:disable RSpecGuide/MinimumBehavioralCoverage
335
+ it { expect(subject.name).to eq('test') }
336
+ end
337
+ ```
338
+
339
+ #### Issue: "Duplicate let" warning but values are contextual
340
+
341
+ **Solution:** This usually indicates poor test hierarchy. Refactor:
342
+
343
+ ```ruby
344
+ # Before - partial duplicates (2/3 contexts)
345
+ describe 'Converter' do
346
+ context 'scenario A' do
347
+ let(:format) { :json }
348
+ # ...
349
+ end
350
+ context 'scenario B' do
351
+ let(:format) { :json } # Duplicate!
352
+ # ...
353
+ end
354
+ context 'scenario C' do
355
+ let(:format) { :xml } # Different
356
+ # ...
357
+ end
358
+ end
359
+
360
+ # After - better hierarchy
361
+ describe 'Converter' do
362
+ context 'with JSON format' do
363
+ let(:format) { :json }
364
+
365
+ context 'scenario A' do
366
+ # ...
367
+ end
368
+
369
+ context 'scenario B' do
370
+ # ...
371
+ end
372
+ end
373
+
374
+ context 'with XML format' do
375
+ let(:format) { :xml }
376
+
377
+ context 'scenario C' do
378
+ # ...
379
+ end
380
+ end
381
+ end
382
+ ```
383
+
384
+ ### Migration Guide
385
+
386
+ #### Upgrading to v0.4.0
387
+
388
+ **Configuration changes:**
389
+
390
+ Starting from v0.4.0, the gem automatically injects its default configuration, including RSpec Language settings. You can simplify your `.rubocop.yml`:
391
+
392
+ ```yaml
393
+ # Before (v0.3.x) - explicit inheritance required
394
+ plugins:
395
+ - rubocop-rspec-guide
396
+
397
+ inherit_gem:
398
+ rubocop-rspec-guide: config/default.yml
399
+
400
+ # After (v0.4.0+) - automatic config injection
401
+ plugins:
402
+ - rubocop-rspec-guide
403
+ ```
404
+
405
+ **What changed:**
406
+ - ✅ `let_it_be` and `let_it_be!` are now automatically recognized (from `test-prof` / `rspec-rails`)
407
+ - ✅ All cops now use `RuboCop::Cop::RSpec::Base` for better RSpec DSL detection
408
+ - ✅ Significant performance improvement: `InvariantExamples` is 4.25x faster
409
+ - ✅ More accurate detection of RSpec constructs
410
+
411
+ **No code changes needed** - your existing RSpec tests will work as before, but with better analysis.
412
+
413
+ #### From CharacteristicsAndContexts to MinimumBehavioralCoverage
414
+
415
+ The old name still works as an alias, but you should update your config:
416
+
417
+ ```yaml
418
+ # Old (deprecated)
419
+ RSpecGuide/CharacteristicsAndContexts:
420
+ Enabled: true
421
+
422
+ # New (recommended)
423
+ RSpecGuide/MinimumBehavioralCoverage:
424
+ Enabled: true
425
+ ```
426
+
427
+ No code changes needed - the cop behavior is the same.
428
+
429
+ #### From DynamicAttributesForTimeAndRandom to DynamicAttributeEvaluation
430
+
431
+ The old name still works as an alias:
432
+
433
+ ```yaml
434
+ # Old (deprecated)
61
435
  FactoryBotGuide/DynamicAttributesForTimeAndRandom:
62
436
  Enabled: true
437
+
438
+ # New (recommended)
439
+ FactoryBotGuide/DynamicAttributeEvaluation:
440
+ Enabled: true
63
441
  ```
64
442
 
443
+ The new cop checks ALL method calls, not just Time/Random, providing better coverage.
444
+
65
445
  ## Cops
66
446
 
67
- ### RSpecGuide/CharacteristicsAndContexts
447
+ ### RSpecGuide/MinimumBehavioralCoverage
68
448
 
69
- Requires at least 2 contexts in a describe block (happy path + edge cases).
449
+ Requires at least 2 behavioral variations in a describe block: either 2+ sibling contexts OR it-blocks + context-blocks.
70
450
 
71
451
  ```ruby
72
452
  # bad
@@ -74,7 +454,7 @@ describe '#calculate' do
74
454
  it 'works' { expect(result).to eq(100) }
75
455
  end
76
456
 
77
- # good
457
+ # good - 2+ contexts
78
458
  describe '#calculate' do
79
459
  context 'with valid data' do
80
460
  it { expect(result).to eq(100) }
@@ -84,8 +464,21 @@ describe '#calculate' do
84
464
  it { expect(result).to be_error }
85
465
  end
86
466
  end
467
+
468
+ # good - it-blocks + context-blocks
469
+ describe '#calculate' do
470
+ it 'works with defaults' do
471
+ expect(result).to eq(100)
472
+ end
473
+
474
+ context 'with invalid data' do
475
+ it { expect(result).to be_error }
476
+ end
477
+ end
87
478
  ```
88
479
 
480
+ **Note:** The old name `RSpecGuide/CharacteristicsAndContexts` is deprecated but still works as an alias.
481
+
89
482
  ### RSpecGuide/HappyPathFirst
90
483
 
91
484
  Ensures corner cases are not placed before happy paths.
@@ -255,24 +648,44 @@ context 'A' do
255
648
  end
256
649
  ```
257
650
 
258
- ### FactoryBotGuide/DynamicAttributesForTimeAndRandom
651
+ ### FactoryBotGuide/DynamicAttributeEvaluation
259
652
 
260
- Ensures time and random values are wrapped in blocks.
653
+ Ensures method calls in factory attributes are wrapped in blocks for dynamic evaluation.
261
654
 
262
655
  ```ruby
263
- # bad
656
+ # bad - method calls evaluated once at factory load time
264
657
  factory :user do
265
- created_at Time.now # evaluated once!
658
+ created_at Time.now # same timestamp for all users!
266
659
  token SecureRandom.hex # same token for all users!
660
+ expires_at 1.day.from_now # same expiry for all users!
661
+ tags Array.new # same array instance shared!
267
662
  end
268
663
 
269
- # good
664
+ # good - wrapped in blocks for dynamic evaluation
270
665
  factory :user do
271
666
  created_at { Time.now }
272
667
  token { SecureRandom.hex }
668
+ expires_at { 1.day.from_now }
669
+ tags { Array.new }
670
+ name "John" # static values are OK
273
671
  end
274
672
  ```
275
673
 
674
+ **Note:** The old name `FactoryBotGuide/DynamicAttributesForTimeAndRandom` is deprecated but still works as an alias.
675
+
676
+ ## Documentation
677
+
678
+ Full API documentation is available:
679
+
680
+ - **Generate locally**: `bundle exec rake doc`
681
+ - **View documentation**: Open `doc/index.html` in your browser
682
+ - **Quick open**: `bundle exec rake doc_open`
683
+
684
+ The documentation includes:
685
+ - Detailed cop descriptions with examples
686
+ - Configuration options for each cop
687
+ - API reference for all classes and modules
688
+
276
689
  ## Development
277
690
 
278
691
  After checking out the repo:
@@ -282,10 +695,24 @@ bundle install
282
695
  bundle exec rspec
283
696
  ```
284
697
 
698
+ Generate documentation:
699
+
700
+ ```bash
701
+ bundle exec rake doc
702
+ ```
703
+
704
+ Run benchmarks:
705
+
706
+ ```bash
707
+ bundle exec rake benchmark:quick
708
+ ```
709
+
285
710
  ## Contributing
286
711
 
287
712
  Bug reports and pull requests are welcome on GitHub.
288
713
 
714
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
715
+
289
716
  ## License
290
717
 
291
718
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -8,3 +8,52 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  require "standard/rake"
9
9
 
10
10
  task default: %i[spec standard]
11
+
12
+ namespace :benchmark do
13
+ desc "Run quick benchmarks for all cops (fast feedback, ~1 minute)"
14
+ task :quick do
15
+ ruby "benchmark/cops_benchmark.rb"
16
+ end
17
+
18
+ desc "Run performance benchmarks for all cops"
19
+ task :cops do
20
+ ruby "benchmark/cops_benchmark.rb"
21
+ end
22
+
23
+ desc "Run scalability benchmarks"
24
+ task :scalability do
25
+ ruby "benchmark/scalability_benchmark.rb"
26
+ end
27
+
28
+ desc "Run all benchmarks in quick mode (~2 minutes)"
29
+ task all: [:cops, :scalability]
30
+
31
+ desc "Run full benchmarks with accurate measurements (~5 minutes)"
32
+ task :full do
33
+ ENV["FULL_BENCHMARK"] = "1"
34
+ Rake::Task["benchmark:all"].invoke
35
+ end
36
+ end
37
+
38
+ desc "Run all benchmarks in quick mode"
39
+ task benchmark: "benchmark:all"
40
+
41
+ begin
42
+ require "yard"
43
+
44
+ YARD::Rake::YardocTask.new(:doc) do |t|
45
+ t.files = ["lib/**/*.rb"]
46
+ t.options = ["--output-dir", "doc", "--readme", "README.md"]
47
+ end
48
+
49
+ desc "Generate documentation and open in browser"
50
+ task doc_open: :doc do
51
+ system("open doc/index.html") || system("xdg-open doc/index.html")
52
+ end
53
+ rescue LoadError
54
+ # YARD not available, skip doc tasks
55
+ desc "Generate YARD documentation (YARD not installed)"
56
+ task :doc do
57
+ abort "YARD is not available. Install it with: gem install yard"
58
+ end
59
+ end