-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Introduce have_reported_error matcher #2849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
8b837a8
to
d2d0e51
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
|
||
```ruby | ||
# passes if Rails.error.report was called with any error | ||
expect { Rails.error.report(StandardError.new) }.to have_reported_error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this file is supposed to contain just the outline of the matchers defined here (and at a glance it’s rather incomplete). What do you think of keeping just one example of usage of this matcher? The rest will be taken into the same documentation from the feature file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very verbose for us... These lines take up half this file when our primary documentation is the feature file itself...
b38ff1c
to
87ebd2a
Compare
71685ad
to
6135d5d
Compare
Scenario: Using in controller specs | ||
Given a file named "spec/controllers/users_controller_spec.rb" with: | ||
"""ruby | ||
require "rails_helper" | ||
|
||
RSpec.describe UsersController, type: :controller do | ||
describe "POST #create" do | ||
it "reports validation errors" do | ||
expect { | ||
post :create, params: { user: { email: "invalid" } } | ||
}.to have_reported_error(ValidationError) | ||
end | ||
end | ||
end | ||
""" | ||
When I run `rspec spec/controllers/users_controller_spec.rb` | ||
Then the examples should all pass | ||
|
||
Scenario: Using in request specs | ||
Given a file named "spec/requests/users_spec.rb" with: | ||
"""ruby | ||
require "rails_helper" | ||
|
||
RSpec.describe "Users", type: :request do | ||
describe "POST /users" do | ||
it "reports processing errors" do | ||
expect { | ||
post "/users", params: { user: { name: "Test" } } | ||
}.to have_reported_error.with(context: "user_creation") | ||
end | ||
end | ||
end | ||
""" | ||
When I run `rspec spec/requests/users_spec.rb` | ||
Then the examples should all pass | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a controller / request specific matcher so these feel out of place, why these, why not every aspect of Rails... I'd just cut them.
👋 I think this would be a great addition, sorry for the size of the review its been on my todo list for a while, its mostly just grammar / wording tweaks plus a few "fit" things. The one change I do want to see though is dropping instance matching, I don't think it makes sense over just providing class / message / using with. |
Commit grammar improvements Co-authored-by: Jon Rowe <[email protected]>
@JonRowe thanks for review.
Which of these you would rather go with? or
?? I assume, that first option is prefered to keep similarity with |
Given that you already have
But if you insisted on |
It makes sense to separate matching of args passed to |
605ab75
to
bd4e048
Compare
@expected_error = nil | ||
@expected_message = expected_message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the first argument was not passed at all and defaulted to UNDEFINED
, the second, expected_message
will also be nil
?
@expected_error = nil | |
@expected_message = expected_message | |
@expected_error = @expected_message = nil |
Rails.error.report(StandardError.new("test"), context: { user_id: 123, context: "test" }) | ||
}.to have_reported_error.with_context(user_id: 123, context: "test") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nested context
strikes back.
it "passes with partial attribute matching" do | ||
expect { | ||
Rails.error.report( | ||
StandardError.new("test"), context: { user_id: 123, context: "test", extra: "data" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, too
expect { | ||
Rails.error.report(StandardError.new("actual message")) | ||
}.to have_reported_error("expected message") | ||
}.to fail_with(/Expected error message to be 'expected message', but got: StandardError/) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that all the failure message? Does it include the actual exception message? Or is it just skipped for brevity in this expectation?
end | ||
|
||
def self.process_with_context | ||
Rails.error.report(ArgumentError.new("Invalid input"), severity: :error , context: { topic: "user_processing"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rails.error.report(ArgumentError.new("Invalid input"), severity: :error , context: { topic: "user_processing"}) | |
Rails.error.report(ArgumentError.new("Invalid input"), severity: :error, context: { topic: "user_processing" }) |
private | ||
|
||
def error_matches_expectation? | ||
return true if @expected_error.nil? && @expected_message.nil? && @reports.count.positive? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return true if @expected_error.nil? && @expected_message.nil? && @reports.count.positive? | |
return true if @expected_error.nil? && @expected_message.nil? |
end | ||
|
||
def failure_message_when_negated | ||
warn_about_negated_with_qualifiers! if has_qualifiers? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be no failure due to a false positive.
Please move to does_not_match?
like here.
@reports.find do |report| | ||
error_class_matches?(report.error) && error_message_matches?(report.error) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if there were two errors reported, same class, same message, but the attributes (context) only match on the second one, not the first one that find
finds?
There is some repetition between this method and those lines.
@attributes.all? do |key, value| | ||
actual_value = actual[key] || actual[key.to_s] || actual[key.to_sym] | ||
if value.respond_to?(:matches?) | ||
value.matches?(actual_value) | ||
else | ||
actual_value == value | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this, should fit nicely here:
@attributes.all? do |key, value| | |
actual_value = actual[key] || actual[key.to_s] || actual[key.to_sym] | |
if value.respond_to?(:matches?) | |
value.matches?(actual_value) | |
else | |
actual_value == value | |
end | |
end | |
values_match?(@attributes, actual) |
There is a difference, though - we don't coerce keys to strings. I'm more inclined to be strict here and don't coerce unless you have a good argument for being indifferent to keys' type.
def unmatched_attributes(actual) | ||
@attributes.reject do |key, value| | ||
actual_value = actual[key] || actual[key.to_s] || actual[key.to_sym] | ||
if value.respond_to?(:matches?) | ||
value.matches?(actual_value) | ||
else | ||
actual_value == value | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just report the entire actual context for simplicity, not the diff?
I apologise for some code review notes that felt off - I had an outdated commit open in one of the tabs 🙏 |
end | ||
|
||
def failure_message | ||
if [email protected]? && [email protected]? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if !@reports.empty? && !@attributes.empty? | |
if @reports.any? && @attributes.any? |
|
||
def failure_message | ||
if [email protected]? && [email protected]? | ||
report_context = @reports.last.context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last
is incorrect if several errors could have been reported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there were no errors reported matching by class/message, even if the expectation has a with_context
qualifier, don't even mention context attributes in the failure message.
But if some reports did match by class/message (or ALL if the expectation did not specify class/message/regexp), list those errors with their attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matching_reports
(plural) should do better
end | ||
|
||
def actual_error | ||
@actual_error ||= matching_report&.error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@actual_error ||= matching_report&.error | |
matching_report&.error |
Memoization feels redundant
end | ||
else | ||
if @expected_error && !actual_error.is_a?(@expected_error) | ||
return "Expected error to be an instance of #{@expected_error}, but got #{actual_error.class} with message: '#{actual_error.message}'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work?
actual_error
is matching_report&.error
where matching_report
is:
@reports.find do |report|
error_class_matches?(report.error) && error_message_matches?(report.error)
and error_class_matches?
is:
@expected_error.nil? || error.is_a?(@expected_error)
Above, we check that @expected_error
is not nil
.
How on Earth can actual_error
not be of @expected_error
class?
Reduced:
actual_error.is_a?(@expected_error) && !actual_error.is_a?(@expected_error)
How does that spec pass?
Co-authored-by: Phil Pirozhkov <[email protected]>
ee09016
to
b5568a0
Compare
fixes #2827
rspec-rails is missing support for Rails ErrorReporter, this was introduced to rails in v7 and has been evolving ever since. With my client, we have moved to using ErrorReporter as a unified error reporting interface, so we can easily move from one error tracking software to another with minimal code changes. And we had a need to test this interface with rspec, so we implemented our own matcher to handle this.
I'm suggesting our internal implementation as is. This is probably not suitable as is for this gem, but I'd like to open discussion with this starting point.
Example usage