diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d48b8a2..37cec1b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix a false positive for `RSpec/ReceiveNever` cop when `allow(...).to receive(...).never`. ([@ydah]) - Fix detection of nameless doubles with methods in `RSpec/VerifiedDoubles`. ([@ushi-as]) - Improve an offense message for `RSpec/RepeatedExample` cop. ([@ydah]) +- Improve `RSpec/MultipleExpectations` message to contextually suggest using `aggregate_failures` when appropriate. ([@svgr-slth]) - Let `RSpec/SpecFilePathFormat` leverage ActiveSupport inflections when configured. ([@corsonknowles], [@bquorning]) ## 3.7.0 (2025-09-01) diff --git a/lib/rubocop/cop/rspec/multiple_expectations.rb b/lib/rubocop/cop/rspec/multiple_expectations.rb index bb1a32297..929bf536a 100644 --- a/lib/rubocop/cop/rspec/multiple_expectations.rb +++ b/lib/rubocop/cop/rspec/multiple_expectations.rb @@ -68,9 +68,12 @@ module RSpec # class MultipleExpectations < Base MSG = 'Example has too many expectations [%d/%d].' + MSG_SUGGEST_AGGREGATE = 'Example has too many expectations [%d/%d]. ' \ + 'Consider using `aggregate_failures` if these expectations are logically related.' ANYTHING = ->(_node) { true } TRUE_NODE = lambda(&:true_type?) + FALSE_NODE = lambda(&:false_type?) exclude_limit 'Max' @@ -101,7 +104,8 @@ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler self.max = expectations_count - flag_example(node, expectation_count: expectations_count) + flag_example(node, expectation_count: expectations_count, + aggregate_failures_disabled: aggregate_failures_disabled?(node)) end private @@ -113,6 +117,13 @@ def example_with_aggregate_failures?(example_node) aggregate_failures?(node_with_aggregate_failures, TRUE_NODE) end + def aggregate_failures_disabled?(example_node) + node_with_aggregate_failures = find_aggregate_failures(example_node) + return false unless node_with_aggregate_failures + + aggregate_failures?(node_with_aggregate_failures, FALSE_NODE) + end + def find_aggregate_failures(example_node) example_node.send_node.each_ancestor(:block) .find { |block_node| aggregate_failures?(block_node, ANYTHING) } @@ -129,11 +140,12 @@ def find_expectation(node, &block) end end - def flag_example(node, expectation_count:) + def flag_example(node, expectation_count:, aggregate_failures_disabled:) + message_template = aggregate_failures_disabled ? MSG : MSG_SUGGEST_AGGREGATE add_offense( node.send_node, message: format( - MSG, + message_template, total: expectation_count, max: max_expectations ) diff --git a/spec/rubocop/cop/rspec/multiple_expectations_spec.rb b/spec/rubocop/cop/rspec/multiple_expectations_spec.rb index 32e8a43a0..66398b243 100644 --- a/spec/rubocop/cop/rspec/multiple_expectations_spec.rb +++ b/spec/rubocop/cop/rspec/multiple_expectations_spec.rb @@ -8,7 +8,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect twice' do - ^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. expect(foo).to eq(bar) expect(baz).to eq(bar) end @@ -34,7 +34,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect_any_instance_of twice' do - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. expect_any_instance_of(Foo).to receive(:bar) expect_any_instance_of(Foo).to receive(:baz) end @@ -46,7 +46,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect_any_instance_of twice' do - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. is_expected.to receive(:bar) is_expected.to receive(:baz) end @@ -58,7 +58,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect with block twice' do - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. expect { something }.to change(Foo.count) expect { something }.to change(Bar.count) end @@ -83,7 +83,7 @@ expect_offense(<<~RUBY) describe Foo do it 'has multiple aggregate_failures calls' do - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. aggregate_failures do end aggregate_failures do @@ -179,7 +179,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect twice' do - ^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. + ^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [2/1]. Consider using `aggregate_failures` if these expectations are logically related. expect(foo).to eq(bar) expect(baz).to eq(bar) end @@ -228,7 +228,7 @@ expect_offense(<<~RUBY) describe Foo do it 'uses expect three times' do - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [3/2]. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Example has too many expectations [3/2]. Consider using `aggregate_failures` if these expectations are logically related. expect(foo).to eq(bar) expect(baz).to eq(bar) expect(qux).to eq(bar)