rubocop-rspec-guide 0.2.1 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -6
- data/.yardopts +9 -0
- data/CHANGELOG.md +92 -0
- data/CONTRIBUTING.md +358 -0
- data/INTEGRATION_TESTING.md +324 -0
- data/README.md +443 -16
- data/Rakefile +49 -0
- data/benchmark/README.md +349 -0
- data/benchmark/baseline_v0.3.1.txt +67 -0
- data/benchmark/baseline_v0.4.0.txt +167 -0
- data/benchmark/benchmark_helper.rb +92 -0
- data/benchmark/compare_versions.rb +136 -0
- data/benchmark/cops_benchmark.rb +428 -0
- data/benchmark/cops_performance.rb +109 -0
- data/benchmark/quick_comparison.rb +58 -0
- data/benchmark/quick_invariant_bench.rb +52 -0
- data/benchmark/rspec_base_integration.rb +86 -0
- data/benchmark/save_baseline.rb +18 -0
- data/benchmark/scalability_benchmark.rb +181 -0
- data/config/default.yml +43 -2
- data/config/obsoletion.yml +6 -0
- data/lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb +193 -0
- data/lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb +10 -106
- data/lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb +13 -78
- data/lib/rubocop/cop/rspec_guide/context_setup.rb +81 -30
- data/lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb +89 -22
- data/lib/rubocop/cop/rspec_guide/duplicate_let_values.rb +91 -22
- data/lib/rubocop/cop/rspec_guide/happy_path_first.rb +52 -21
- data/lib/rubocop/cop/rspec_guide/invariant_examples.rb +60 -19
- data/lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb +165 -0
- data/lib/rubocop/rspec/guide/inject.rb +26 -0
- data/lib/rubocop/rspec/guide/plugin.rb +45 -0
- data/lib/rubocop/rspec/guide/version.rb +1 -1
- data/lib/rubocop-rspec-guide.rb +4 -0
- metadata +49 -1
|
@@ -1,120 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "dynamic_attribute_evaluation"
|
|
4
|
+
|
|
3
5
|
module RuboCop
|
|
4
6
|
module Cop
|
|
5
7
|
module FactoryBotGuide
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
+
# @deprecated Use `DynamicAttributeEvaluation` instead.
|
|
9
|
+
# This cop has been renamed to better reflect its broader scope.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# factory :user do
|
|
12
|
-
# created_at Time.now
|
|
13
|
-
# token SecureRandom.hex
|
|
14
|
-
# expires_at 1.day.from_now
|
|
15
|
-
# end
|
|
11
|
+
# Checks that method calls in FactoryBot attribute definitions
|
|
12
|
+
# are wrapped in blocks for dynamic evaluation.
|
|
16
13
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# created_at { Time.now }
|
|
20
|
-
# token { SecureRandom.hex }
|
|
21
|
-
# expires_at { 1.day.from_now }
|
|
22
|
-
# name "John" # Static values are OK
|
|
23
|
-
# end
|
|
14
|
+
# This cop is deprecated and will be removed in a future release.
|
|
15
|
+
# Please use `FactoryBotGuide/DynamicAttributeEvaluation` instead.
|
|
24
16
|
#
|
|
25
|
-
class DynamicAttributesForTimeAndRandom <
|
|
17
|
+
class DynamicAttributesForTimeAndRandom < DynamicAttributeEvaluation
|
|
18
|
+
# Override to provide deprecation warning with the old cop name
|
|
26
19
|
MSG = "Use block syntax for attribute `%<attribute>s` because `%<method>s` " \
|
|
27
20
|
"is evaluated once at factory definition time. " \
|
|
28
21
|
"Wrap in block: `%<attribute>s { %<value>s }`"
|
|
29
|
-
|
|
30
|
-
TIME_CLASSES = %w[Time Date DateTime].freeze
|
|
31
|
-
RANDOM_CLASSES = %w[SecureRandom].freeze
|
|
32
|
-
|
|
33
|
-
# @!method factory_block?(node)
|
|
34
|
-
def_node_matcher :factory_block?, <<~PATTERN
|
|
35
|
-
(block
|
|
36
|
-
(send {nil? (const {nil? cbase} :FactoryBot)} :factory ...)
|
|
37
|
-
...)
|
|
38
|
-
PATTERN
|
|
39
|
-
|
|
40
|
-
# @!method attribute_assignment?(node)
|
|
41
|
-
def_node_matcher :attribute_assignment?, <<~PATTERN
|
|
42
|
-
(send nil? $_ $_value)
|
|
43
|
-
PATTERN
|
|
44
|
-
|
|
45
|
-
def on_block(node)
|
|
46
|
-
return unless factory_block?(node)
|
|
47
|
-
|
|
48
|
-
# Check all attribute assignments within the factory
|
|
49
|
-
node.each_descendant(:send) do |send_node|
|
|
50
|
-
check_attribute(send_node)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def check_attribute(node)
|
|
57
|
-
attribute_assignment?(node) do |attribute_name, value|
|
|
58
|
-
# Skip if value is already a block
|
|
59
|
-
next if value.block_type?
|
|
60
|
-
|
|
61
|
-
# Check if the value is a dangerous method call
|
|
62
|
-
next unless dangerous_method_call?(value)
|
|
63
|
-
|
|
64
|
-
add_offense(
|
|
65
|
-
node,
|
|
66
|
-
message: format(
|
|
67
|
-
MSG,
|
|
68
|
-
attribute: attribute_name,
|
|
69
|
-
method: method_description(value),
|
|
70
|
-
value: value.source
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def dangerous_method_call?(node)
|
|
77
|
-
# Only method calls are potentially dangerous
|
|
78
|
-
return false unless node.send_type?
|
|
79
|
-
|
|
80
|
-
# Time.now, Date.today, DateTime.now, etc.
|
|
81
|
-
return true if time_method?(node)
|
|
82
|
-
|
|
83
|
-
# SecureRandom.hex, SecureRandom.uuid, etc.
|
|
84
|
-
return true if random_method?(node)
|
|
85
|
-
|
|
86
|
-
# Any other method calls (e.g., 1.day.ago, Array.new, etc.)
|
|
87
|
-
# are evaluated at factory load time
|
|
88
|
-
true
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def time_method?(node)
|
|
92
|
-
return false unless node.receiver
|
|
93
|
-
|
|
94
|
-
receiver_name = if node.receiver.const_type?
|
|
95
|
-
node.receiver.const_name
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
TIME_CLASSES.include?(receiver_name)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def random_method?(node)
|
|
102
|
-
return false unless node.receiver
|
|
103
|
-
|
|
104
|
-
receiver_name = if node.receiver.const_type?
|
|
105
|
-
node.receiver.const_name
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
RANDOM_CLASSES.include?(receiver_name)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def method_description(node)
|
|
112
|
-
if node.receiver
|
|
113
|
-
"#{node.receiver.source}.#{node.method_name}"
|
|
114
|
-
else
|
|
115
|
-
node.method_name.to_s
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
22
|
end
|
|
119
23
|
end
|
|
120
24
|
end
|
|
@@ -1,90 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "minimum_behavioral_coverage"
|
|
4
|
+
|
|
3
5
|
module RuboCop
|
|
4
6
|
module Cop
|
|
5
7
|
module RSpecGuide
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @example
|
|
10
|
-
# # bad
|
|
11
|
-
# describe '#process' do
|
|
12
|
-
# it 'works' do
|
|
13
|
-
# expect(subject.process).to be_success
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
#
|
|
17
|
-
# # bad
|
|
18
|
-
# describe '#process' do
|
|
19
|
-
# context 'when data is valid' do
|
|
20
|
-
# it 'processes' do
|
|
21
|
-
# expect(subject.process).to be_success
|
|
22
|
-
# end
|
|
23
|
-
# end
|
|
24
|
-
# end
|
|
8
|
+
# @deprecated Use `MinimumBehavioralCoverage` instead.
|
|
9
|
+
# This cop has been renamed to better reflect its purpose.
|
|
25
10
|
#
|
|
26
|
-
#
|
|
27
|
-
# describe '#process' do
|
|
28
|
-
# context 'when data is valid' do
|
|
29
|
-
# it 'processes successfully' do
|
|
30
|
-
# expect(subject.process).to be_success
|
|
31
|
-
# end
|
|
32
|
-
# end
|
|
11
|
+
# Checks that describe blocks test at least 2 behavioral variations.
|
|
33
12
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# expect(subject.process).to be_error
|
|
37
|
-
# end
|
|
38
|
-
# end
|
|
39
|
-
# end
|
|
13
|
+
# This cop is deprecated and will be removed in a future release.
|
|
14
|
+
# Please use `RSpecGuide/MinimumBehavioralCoverage` instead.
|
|
40
15
|
#
|
|
41
|
-
class CharacteristicsAndContexts <
|
|
42
|
-
|
|
43
|
-
|
|
16
|
+
class CharacteristicsAndContexts < MinimumBehavioralCoverage
|
|
17
|
+
# Override to provide deprecation warning with the old cop name
|
|
18
|
+
MSG = "Describe block should test at least 2 behavioral variations: " \
|
|
19
|
+
"either use 2+ sibling contexts (happy path + edge cases), " \
|
|
20
|
+
"or combine it-blocks for default behavior with context-blocks for edge cases. " \
|
|
44
21
|
"Use `# rubocop:disable RSpecGuide/CharacteristicsAndContexts` " \
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
# @!method describe_block?(node)
|
|
48
|
-
def_node_matcher :describe_block?, <<~PATTERN
|
|
49
|
-
(block
|
|
50
|
-
(send nil? :describe ...)
|
|
51
|
-
...)
|
|
52
|
-
PATTERN
|
|
53
|
-
|
|
54
|
-
# @!method context_block?(node)
|
|
55
|
-
def_node_matcher :context_block?, <<~PATTERN
|
|
56
|
-
(block (send nil? :context ...) ...)
|
|
57
|
-
PATTERN
|
|
58
|
-
|
|
59
|
-
def on_block(node)
|
|
60
|
-
return unless describe_block?(node)
|
|
61
|
-
|
|
62
|
-
# Collect direct child context blocks
|
|
63
|
-
contexts = collect_sibling_contexts(node)
|
|
64
|
-
|
|
65
|
-
# Add offense if less than 2 contexts
|
|
66
|
-
add_offense(node) if contexts.size < 2
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def collect_sibling_contexts(node)
|
|
72
|
-
# The body of a describe/context block may be:
|
|
73
|
-
# 1. A single block node (if only one child)
|
|
74
|
-
# 2. A begin node containing multiple children
|
|
75
|
-
body = node.body
|
|
76
|
-
return [] unless body
|
|
77
|
-
|
|
78
|
-
if body.begin_type?
|
|
79
|
-
# Multiple children wrapped in begin node
|
|
80
|
-
body.children.select { |child| child.block_type? && context_block?(child) }
|
|
81
|
-
elsif body.block_type? && context_block?(body)
|
|
82
|
-
# Single context child
|
|
83
|
-
[body]
|
|
84
|
-
else
|
|
85
|
-
[]
|
|
86
|
-
end
|
|
87
|
-
end
|
|
22
|
+
"for simple cases (e.g., getters) with no edge cases."
|
|
88
23
|
end
|
|
89
24
|
end
|
|
90
25
|
end
|
|
@@ -6,58 +6,108 @@ module RuboCop
|
|
|
6
6
|
# Checks that context blocks have setup (let/before) to distinguish
|
|
7
7
|
# them from the parent context.
|
|
8
8
|
#
|
|
9
|
+
# Contexts exist to test different scenarios or states. Without explicit setup,
|
|
10
|
+
# the context doesn't actually change anything from its parent, making the
|
|
11
|
+
# context boundary meaningless and confusing.
|
|
12
|
+
#
|
|
13
|
+
# Valid setup methods: let, let!, let_it_be, let_it_be!, before
|
|
14
|
+
#
|
|
9
15
|
# Note: subject should be defined at describe level, not in contexts,
|
|
10
16
|
# as it describes the object under test, not context-specific state.
|
|
11
17
|
# Use RSpec/LeadingSubject cop to ensure subject is defined first.
|
|
12
18
|
#
|
|
13
|
-
# @
|
|
14
|
-
#
|
|
19
|
+
# @safety
|
|
20
|
+
# This cop is safe to run automatically. It only checks for presence
|
|
21
|
+
# of setup, not for semantic correctness.
|
|
22
|
+
#
|
|
23
|
+
# @example Bad - no setup
|
|
24
|
+
# # bad - context has no setup, so what's different?
|
|
15
25
|
# context 'when user is premium' do
|
|
16
|
-
# it
|
|
17
|
-
#
|
|
18
|
-
#
|
|
26
|
+
# it { expect(user).to have_access }
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # bad - subject in context (should be in describe)
|
|
30
|
+
# context 'with custom config' do
|
|
31
|
+
# subject { Calculator.new(config) } # Wrong place!
|
|
32
|
+
# it { is_expected.to be_valid }
|
|
19
33
|
# end
|
|
20
34
|
#
|
|
21
|
-
#
|
|
35
|
+
# @example Good - using let
|
|
36
|
+
# # good - let defines context-specific state
|
|
22
37
|
# context 'when user is premium' do
|
|
23
38
|
# let(:user) { create(:user, :premium) }
|
|
39
|
+
# it { expect(user).to have_access }
|
|
40
|
+
# end
|
|
24
41
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
42
|
+
# # good - let! for immediate evaluation
|
|
43
|
+
# context 'with existing records' do
|
|
44
|
+
# let!(:records) { create_list(:record, 3) }
|
|
45
|
+
# it { expect(Record.count).to eq(3) }
|
|
28
46
|
# end
|
|
29
47
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
48
|
+
# @example Good - using let_it_be (test-prof/rspec-rails)
|
|
49
|
+
# # good - let_it_be for performance (created once per context)
|
|
50
|
+
# context 'with many users' do
|
|
51
|
+
# let_it_be(:users) { create_list(:user, 100) }
|
|
52
|
+
# it { expect(users.size).to eq(100) }
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# # good - let_it_be! for immediate evaluation
|
|
56
|
+
# context 'with frozen time' do
|
|
57
|
+
# let_it_be!(:timestamp) { Time.current }
|
|
58
|
+
# it { expect(timestamp).to be_frozen }
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# @example Good - using before
|
|
62
|
+
# # good - before modifies existing state
|
|
63
|
+
# context 'when user is upgraded' do
|
|
32
64
|
# before { user.upgrade_to_premium! }
|
|
65
|
+
# it { expect(user).to be_premium }
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# # good - before with multiple setup steps
|
|
69
|
+
# context 'with configured environment' do
|
|
70
|
+
# before do
|
|
71
|
+
# allow(ENV).to receive(:[]).with('API_KEY').and_return('test-key')
|
|
72
|
+
# allow(ENV).to receive(:[]).with('API_URL').and_return('http://test')
|
|
73
|
+
# end
|
|
74
|
+
# it { expect(api_client).to be_configured }
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# @example Subject placement
|
|
78
|
+
# # bad - subject in context
|
|
79
|
+
# describe Calculator do
|
|
80
|
+
# context 'with custom config' do
|
|
81
|
+
# subject { Calculator.new(custom_config) } # Wrong!
|
|
82
|
+
# it { is_expected.to be_valid }
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# # good - subject in describe, config in context
|
|
87
|
+
# describe Calculator do
|
|
88
|
+
# subject { Calculator.new(config) }
|
|
33
89
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
90
|
+
# context 'with custom config' do
|
|
91
|
+
# let(:config) { CustomConfig.new } # Right!
|
|
92
|
+
# it { is_expected.to be_valid }
|
|
36
93
|
# end
|
|
37
94
|
# end
|
|
38
95
|
#
|
|
39
|
-
class ContextSetup < Base
|
|
96
|
+
class ContextSetup < RuboCop::Cop::RSpec::Base
|
|
40
97
|
MSG = "Context should have setup (let/let!/let_it_be/let_it_be!/before) to distinguish it from parent context"
|
|
41
98
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
(block
|
|
45
|
-
(send nil? :context ...)
|
|
46
|
-
...)
|
|
47
|
-
PATTERN
|
|
48
|
-
|
|
49
|
-
# @!method let_declaration?(node)
|
|
50
|
-
def_node_matcher :let_declaration?, <<~PATTERN
|
|
51
|
-
(block (send nil? {:let :let! :let_it_be :let_it_be!} ...) ...)
|
|
52
|
-
PATTERN
|
|
99
|
+
# Using rubocop-rspec API: let?(node) and hook?(node) from Base
|
|
100
|
+
# Custom matcher for context-only:
|
|
53
101
|
|
|
54
|
-
# @!method
|
|
55
|
-
def_node_matcher :
|
|
56
|
-
(block (send nil? :
|
|
102
|
+
# @!method context_only?(node)
|
|
103
|
+
def_node_matcher :context_only?, <<~PATTERN
|
|
104
|
+
(block (send nil? :context ...) ...)
|
|
57
105
|
PATTERN
|
|
58
106
|
|
|
59
107
|
def on_block(node)
|
|
60
|
-
|
|
108
|
+
# Fast pre-check: only process context blocks
|
|
109
|
+
return unless node.method?(:context)
|
|
110
|
+
return unless context_only?(node)
|
|
61
111
|
|
|
62
112
|
# Check if context has at least one setup node (let or before)
|
|
63
113
|
# Note: subject is NOT counted as context setup because it describes
|
|
@@ -77,7 +127,8 @@ module RuboCop
|
|
|
77
127
|
(block_node.parent.begin_type? && block_node.parent.parent == context_node)
|
|
78
128
|
next unless is_immediate_child
|
|
79
129
|
|
|
80
|
-
|
|
130
|
+
# Use rubocop-rspec API matchers
|
|
131
|
+
return true if let?(block_node) || (hook?(block_node) && block_node.method?(:before))
|
|
81
132
|
end
|
|
82
133
|
|
|
83
134
|
false
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module RSpecGuide
|
|
6
|
-
# Detects duplicate before hooks with identical code across
|
|
7
|
-
# These should be extracted to the parent context.
|
|
6
|
+
# Detects duplicate before hooks with identical code across sibling contexts.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# When the same before hook appears in multiple sibling contexts,
|
|
9
|
+
# it indicates one of two problems:
|
|
10
|
+
# 1. ERROR: Duplicate in ALL contexts → must extract to parent
|
|
11
|
+
# 2. WARNING: Duplicate in SOME contexts → suggests poor test hierarchy
|
|
12
|
+
#
|
|
13
|
+
# @safety
|
|
14
|
+
# This cop is safe to run automatically. It compares hook bodies for
|
|
15
|
+
# exact matches, not semantic equivalence.
|
|
16
|
+
#
|
|
17
|
+
# @example ERROR - duplicate in ALL contexts
|
|
18
|
+
# # bad - before hook duplicated in all 2 contexts
|
|
11
19
|
# describe 'Controller' do
|
|
12
20
|
# context 'as admin' do
|
|
13
21
|
# before { sign_in(user) }
|
|
@@ -15,14 +23,14 @@ module RuboCop
|
|
|
15
23
|
# end
|
|
16
24
|
#
|
|
17
25
|
# context 'as guest' do
|
|
18
|
-
# before { sign_in(user) } #
|
|
26
|
+
# before { sign_in(user) } # ERROR: in all contexts!
|
|
19
27
|
# it { expect(response).to be_forbidden }
|
|
20
28
|
# end
|
|
21
29
|
# end
|
|
22
30
|
#
|
|
23
|
-
# # good
|
|
31
|
+
# # good - extracted to parent
|
|
24
32
|
# describe 'Controller' do
|
|
25
|
-
# before { sign_in(user) } #
|
|
33
|
+
# before { sign_in(user) } # Moved to parent
|
|
26
34
|
#
|
|
27
35
|
# context 'as admin' do
|
|
28
36
|
# it { expect(response).to be_successful }
|
|
@@ -33,33 +41,92 @@ module RuboCop
|
|
|
33
41
|
# end
|
|
34
42
|
# end
|
|
35
43
|
#
|
|
36
|
-
|
|
44
|
+
# @example WARNING - duplicate in SOME contexts
|
|
45
|
+
# # bad - before hook duplicated in 2/3 contexts (code smell)
|
|
46
|
+
# describe 'API' do
|
|
47
|
+
# context 'scenario A' do
|
|
48
|
+
# before { setup_api }
|
|
49
|
+
# it { expect(response).to be_ok }
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# context 'scenario B' do
|
|
53
|
+
# before { setup_api } # WARNING: duplicated in 2/3
|
|
54
|
+
# it { expect(response).to be_ok }
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# context 'scenario C' do
|
|
58
|
+
# before { setup_different_api } # Different setup
|
|
59
|
+
# it { expect(response).to be_ok }
|
|
60
|
+
# end
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# # good - refactor hierarchy
|
|
64
|
+
# describe 'API' do
|
|
65
|
+
# context 'with standard setup' do
|
|
66
|
+
# before { setup_api }
|
|
67
|
+
#
|
|
68
|
+
# context 'scenario A' do
|
|
69
|
+
# it { expect(response).to be_ok }
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# context 'scenario B' do
|
|
73
|
+
# it { expect(response).to be_ok }
|
|
74
|
+
# end
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# context 'with different setup' do
|
|
78
|
+
# before { setup_different_api }
|
|
79
|
+
#
|
|
80
|
+
# context 'scenario C' do
|
|
81
|
+
# it { expect(response).to be_ok }
|
|
82
|
+
# end
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# @example Configuration
|
|
87
|
+
# # To disable warnings for partial duplicates:
|
|
88
|
+
# RSpecGuide/DuplicateBeforeHooks:
|
|
89
|
+
# WarnOnPartialDuplicates: false # Only report full duplicates
|
|
90
|
+
#
|
|
91
|
+
# @example Edge case - different hooks
|
|
92
|
+
# # good - different before hooks (no duplicate)
|
|
93
|
+
# describe 'Service' do
|
|
94
|
+
# context 'with user A' do
|
|
95
|
+
# before { sign_in(user_a) }
|
|
96
|
+
# it { expect(service.call).to be_success }
|
|
97
|
+
# end
|
|
98
|
+
#
|
|
99
|
+
# context 'with user B' do
|
|
100
|
+
# before { sign_in(user_b) } # Different, OK
|
|
101
|
+
# it { expect(service.call).to be_success }
|
|
102
|
+
# end
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
class DuplicateBeforeHooks < RuboCop::Cop::RSpec::Base
|
|
37
106
|
MSG_ERROR = "Duplicate `before` hook in ALL sibling contexts. " \
|
|
38
107
|
"Extract to parent context."
|
|
39
108
|
MSG_WARNING = "Before hook duplicated in %<count>d/%<total>d sibling contexts. " \
|
|
40
109
|
"Consider refactoring test hierarchy - this suggests poor organization."
|
|
41
110
|
|
|
42
|
-
#
|
|
43
|
-
|
|
111
|
+
# Using rubocop-rspec API: example_group?(node) and hook?(node) from Base
|
|
112
|
+
# Custom matchers:
|
|
113
|
+
|
|
114
|
+
# @!method context_only?(node)
|
|
115
|
+
def_node_matcher :context_only?, <<~PATTERN
|
|
44
116
|
(block (send nil? :context ...) ...)
|
|
45
117
|
PATTERN
|
|
46
118
|
|
|
47
|
-
# @!method
|
|
48
|
-
def_node_matcher :
|
|
119
|
+
# @!method before_hook_with_body?(node)
|
|
120
|
+
def_node_matcher :before_hook_with_body?, <<~PATTERN
|
|
49
121
|
(block
|
|
50
122
|
(send nil? :before ...)
|
|
51
123
|
(args)
|
|
52
124
|
$_body)
|
|
53
125
|
PATTERN
|
|
54
126
|
|
|
55
|
-
# @!method example_group?(node)
|
|
56
|
-
def_node_matcher :example_group?, <<~PATTERN
|
|
57
|
-
(block
|
|
58
|
-
(send nil? {:describe :context} ...)
|
|
59
|
-
...)
|
|
60
|
-
PATTERN
|
|
61
|
-
|
|
62
127
|
def on_block(node)
|
|
128
|
+
# Fast pre-check: only process describe/context blocks
|
|
129
|
+
return unless node.method?(:describe) || node.method?(:context)
|
|
63
130
|
return unless example_group?(node)
|
|
64
131
|
|
|
65
132
|
# Collect all sibling contexts
|
|
@@ -86,8 +153,8 @@ module RuboCop
|
|
|
86
153
|
|
|
87
154
|
if body.begin_type?
|
|
88
155
|
# Multiple children wrapped in begin node
|
|
89
|
-
body.children.select { |child| child.block_type? &&
|
|
90
|
-
elsif body.block_type? &&
|
|
156
|
+
body.children.select { |child| child.block_type? && context_only?(child) }
|
|
157
|
+
elsif body.block_type? && context_only?(body)
|
|
91
158
|
# Single context child
|
|
92
159
|
[body]
|
|
93
160
|
else
|
|
@@ -106,7 +173,7 @@ module RuboCop
|
|
|
106
173
|
(block_node.parent.begin_type? && block_node.parent.parent == context_node)
|
|
107
174
|
next unless is_immediate_child
|
|
108
175
|
|
|
109
|
-
|
|
176
|
+
before_hook_with_body?(block_node) do |body|
|
|
110
177
|
befores << {body_source: normalize_source(body.source), node: block_node}
|
|
111
178
|
end
|
|
112
179
|
end
|