Skip to content

Commit 1b6deb6

Browse files
committed
have_reported_error to accept class name and a message
1 parent 7259a25 commit 1b6deb6

File tree

3 files changed

+135
-46
lines changed

3 files changed

+135
-46
lines changed

features/matchers/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ expect(assigns(:widget)).to be_a_new(Widget)
2828
### error reporting
2929

3030
```ruby
31-
# passes when `Rails.error.report` is called with specific error instance and message
31+
# passes when any error is reported
32+
expect { Rails.error.report(StandardError.new) }.to have_reported_error
33+
34+
# passes when specific error class is reported
35+
expect { Rails.error.report(MyError.new) }.to have_reported_error(MyError)
36+
37+
# passes when specific error class with exact message is reported
38+
expect { Rails.error.report(MyError.new("message")) }.to have_reported_error(MyError, "message")
39+
40+
# passes when specific error class with message matching pattern is reported
41+
expect { Rails.error.report(MyError.new("test message")) }.to have_reported_error(MyError, /test/)
42+
43+
# passes when error is reported with specific context attributes
44+
expect { Rails.error.report(StandardError.new, context: { user_id: 123 }) }.to have_reported_error.with_context(user_id: 123)
45+
46+
# backward compatibility - accepts error instances
3247
expect { Rails.error.report(MyError.new("message")) }.to have_reported_error(MyError.new("message"))
3348
```

lib/rspec/rails/matchers/have_reported_error.rb

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,24 @@ def report(error, **attrs)
2323
# @api private
2424
# @see RSpec::Rails::Matchers#have_reported_error
2525
class HaveReportedError < RSpec::Rails::Matchers::BaseMatcher
26-
def initialize(expected_error = nil)
27-
@expected_error = expected_error
26+
def initialize(expected_error_class = nil, expected_message = nil)
27+
# Handle backward compatibility with old API
28+
if expected_error_class.is_a?(Exception)
29+
@expected_error_class = expected_error_class.class
30+
@expected_message = expected_error_class.message.empty? ? nil : expected_error_class.message
31+
elsif expected_error_class.is_a?(Regexp)
32+
@expected_error_class = nil
33+
@expected_message = expected_error_class
34+
elsif expected_error_class.is_a?(Symbol)
35+
@expected_error_symbol = expected_error_class
36+
@expected_error_class = nil
37+
@expected_message = nil
38+
else
39+
@expected_error_class = expected_error_class
40+
@expected_message = expected_message
41+
@expected_error_symbol = nil
42+
end
43+
2844
@attributes = {}
2945
@error_subscriber = nil
3046
end
@@ -48,7 +64,7 @@ def matches?(block)
4864

4965
block.call
5066

51-
return false if @error_subscriber.events.empty? && !@expected_error.nil?
67+
return false if @error_subscriber.events.empty?
5268
return false unless error_matches_expectation?
5369

5470
return attributes_match_if_specified?
@@ -62,16 +78,18 @@ def supports_block_expectations?
6278

6379
def description
6480
desc = "report an error"
65-
case @expected_error
66-
when Class
67-
desc = "report a #{@expected_error} error"
68-
when Exception
69-
desc = "report a #{@expected_error.class} error"
70-
desc += " with message '#{@expected_error.message}'" unless @expected_error.message.empty?
71-
when Regexp
72-
desc = "report an error with message matching #{@expected_error}"
73-
when Symbol
74-
desc = "report #{@expected_error}"
81+
if @expected_error_symbol
82+
desc = "report #{@expected_error_symbol}"
83+
elsif @expected_error_class
84+
desc = "report a #{@expected_error_class} error"
85+
end
86+
if @expected_message
87+
case @expected_message
88+
when Regexp
89+
desc += " with message matching #{@expected_message}"
90+
when String
91+
desc += " with message '#{@expected_message}'"
92+
end
7593
end
7694
desc += " with #{@attributes}" unless @attributes.empty?
7795
desc
@@ -87,15 +105,17 @@ def failure_message
87105
elsif @error_subscriber.events.empty?
88106
return 'Expected the block to report an error, but none was reported.'
89107
else
90-
case @expected_error
91-
when Class
92-
return "Expected error to be an instance of #{@expected_error}, but got #{actual_error.class} with message: '#{actual_error.message}'"
93-
when Exception
94-
return "Expected error to be #{@expected_error.class} with message '#{@expected_error.message}', but got #{actual_error.class} with message: '#{actual_error.message}'"
95-
when Regexp
96-
return "Expected error message to match #{@expected_error}, but got: '#{actual_error.message}'"
97-
when Symbol
98-
return "Expected error to be #{@expected_error}, but got: #{actual_error}"
108+
if @expected_error_symbol
109+
return "Expected error to be #{@expected_error_symbol}, but got: #{actual_error}"
110+
elsif @expected_error_class && !actual_error.is_a?(@expected_error_class)
111+
return "Expected error to be an instance of #{@expected_error_class}, but got #{actual_error.class} with message: '#{actual_error.message}'"
112+
elsif @expected_message
113+
case @expected_message
114+
when Regexp
115+
return "Expected error message to match #{@expected_message}, but got: '#{actual_error.message}'"
116+
when String
117+
return "Expected error message to be '#{@expected_message}', but got: '#{actual_error.message}'"
118+
end
99119
else
100120
return "Expected specific error, but got #{actual_error.class} with message: '#{actual_error.message}'"
101121
end
@@ -113,19 +133,30 @@ def failure_message_when_negated
113133
private
114134

115135
def error_matches_expectation?
116-
return true if @expected_error.nil? && @error_subscriber.events.any?
117-
118-
case @expected_error
119-
when Class
120-
actual_error.is_a?(@expected_error)
121-
when Exception
122-
actual_error.is_a?(@expected_error.class) &&
123-
(@expected_error.message.empty? || actual_error.message == @expected_error.message)
124-
when Regexp
125-
actual_error.message&.match(@expected_error)
126-
when Symbol
127-
actual_error == @expected_error
136+
# If no events were reported, we can't match anything
137+
return false if @error_subscriber.events.empty?
138+
139+
# Handle symbol matching (backward compatibility)
140+
if @expected_error_symbol
141+
return actual_error == @expected_error_symbol
142+
end
143+
144+
# If no constraints are given, any error should match
145+
return true if @expected_error_class.nil? && @expected_message.nil?
146+
147+
class_matches = @expected_error_class.nil? || actual_error.is_a?(@expected_error_class)
148+
149+
message_matches = if @expected_message.nil?
150+
true
151+
elsif @expected_message.is_a?(Regexp)
152+
actual_error.message&.match(@expected_message)
153+
elsif @expected_message.is_a?(String)
154+
actual_error.message == @expected_message
155+
else
156+
false
128157
end
158+
159+
class_matches && message_matches
129160
end
130161

131162
def attributes_match_if_specified?
@@ -172,21 +203,26 @@ def unmatched_attributes(actual)
172203
# @example Checking for specific error class
173204
# expect { Rails.error.report(MyError.new) }.to have_reported_error(MyError)
174205
#
175-
# @example Checking for specific error instance with message
206+
# @example Checking for specific error class with message
207+
# expect { Rails.error.report(MyError.new("message")) }.to have_reported_error(MyError, "message")
208+
#
209+
# @example Checking for specific error instance (backward compatibility)
176210
# expect { Rails.error.report(MyError.new("message")) }.to have_reported_error(MyError.new("message"))
177211
#
178212
# @example Checking error attributes
179213
# expect { Rails.error.report(StandardError.new, context: "test") }.to have_reported_error.with_context(context: "test")
180214
#
181215
# @example Checking error message patterns
216+
# expect { Rails.error.report(StandardError.new("test message")) }.to have_reported_error(StandardError, /test/)
182217
# expect { Rails.error.report(StandardError.new("test message")) }.to have_reported_error(/test/)
183218
#
184219
# @example Negation
185220
# expect { "safe code" }.not_to have_reported_error
186221
#
187-
# @param expected_error [Class, Exception, Regexp, Symbol, nil] the expected error to match
188-
def have_reported_error(expected_error = nil)
189-
HaveReportedError.new(expected_error)
222+
# @param expected_error_class [Class, Exception, Regexp, Symbol, nil] the expected error class to match, or error instance for backward compatibility
223+
# @param expected_message [String, Regexp, nil] the expected error message to match
224+
def have_reported_error(expected_error_class = nil, expected_message = nil)
225+
HaveReportedError.new(expected_error_class, expected_message)
190226
end
191227

192228
alias_method :reports_error, :have_reported_error

spec/rspec/rails/matchers/have_reported_error_spec.rb

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,36 +46,36 @@ class AnotherTestError < StandardError; end
4646
it "passes with an error that matches exactly" do
4747
expect {
4848
Rails.error.report(TestError.new("exact message"))
49-
}.to have_reported_error(TestError.new("exact message"))
49+
}.to have_reported_error(TestError, "exact message")
5050
end
5151

52-
it "passes any error of the same class if the expected message is empty" do
52+
it "passes any error of the same class if no message is specified" do
5353
expect {
5454
Rails.error.report(TestError.new("any message"))
55-
}.to have_reported_error(TestError.new(""))
55+
}.to have_reported_error(TestError)
5656
end
5757

5858
it "fails when the error has different message to the expected" do
5959
expect {
6060
expect {
6161
Rails.error.report(TestError.new("actual message"))
62-
}.to have_reported_error(TestError.new("expected message"))
63-
}.to fail_with(/Expected error to be TestError with message 'expected message', but got TestError with message: 'actual message'/)
62+
}.to have_reported_error(TestError, "expected message")
63+
}.to fail_with(/Expected error message to be 'expected message', but got: 'actual message'/)
6464
end
6565
end
6666

6767
context "constrained by regex pattern matching" do
6868
it "passes when an error message matches the pattern" do
6969
expect {
7070
Rails.error.report(StandardError.new("error with pattern"))
71-
}.to have_reported_error(/with pattern/)
71+
}.to have_reported_error(StandardError, /with pattern/)
7272
end
7373

7474
it "fails when no error messages match the pattern" do
7575
expect {
7676
expect {
7777
Rails.error.report(StandardError.new("error without match"))
78-
}.to have_reported_error(/different pattern/)
78+
}.to have_reported_error(StandardError, /different pattern/)
7979
}.to fail_with(/Expected error message to match/)
8080
end
8181
end
@@ -152,6 +152,44 @@ class AnotherTestError < StandardError; end
152152
end
153153
end
154154

155+
context "backward compatibility with old API" do
156+
it "accepts Exception instances and matches class and message" do
157+
expect {
158+
Rails.error.report(TestError.new("exact message"))
159+
}.to have_reported_error(TestError.new("exact message"))
160+
end
161+
162+
it "accepts Exception instances with empty message and matches any message of that class" do
163+
expect {
164+
Rails.error.report(TestError.new("any message"))
165+
}.to have_reported_error(TestError.new(""))
166+
end
167+
168+
it "accepts Regexp directly for message matching" do
169+
expect {
170+
Rails.error.report(StandardError.new("error with pattern"))
171+
}.to have_reported_error(/with pattern/)
172+
end
173+
174+
175+
176+
it "fails when Exception instance class doesn't match" do
177+
expect {
178+
expect {
179+
Rails.error.report(AnotherTestError.new("message"))
180+
}.to have_reported_error(TestError.new("message"))
181+
}.to fail_with(/Expected error to be an instance of TestError, but got AnotherTestError/)
182+
end
183+
184+
it "fails when Exception instance message doesn't match" do
185+
expect {
186+
expect {
187+
Rails.error.report(TestError.new("actual message"))
188+
}.to have_reported_error(TestError.new("expected message"))
189+
}.to fail_with(/Expected error message to be 'expected message', but got: 'actual message'/)
190+
end
191+
end
192+
155193
describe "integration with actual usage patterns" do
156194
it "works with multiple error reports in a block" do
157195
expect {

0 commit comments

Comments
 (0)