-
-
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...
attributes_match?(event_context) | ||
end | ||
|
||
def actual_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.
Cosmetic. [0] and [1] look cryptic, is it worth using a struct?
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.
sounds good!
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 |
c1134da
to
f90fc3b
Compare
f90fc3b
to
2488a71
Compare
end | ||
|
||
def failure_message | ||
if !@error_subscriber.events.empty? && [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.
Cosmetic: any?
or present?
?
|
||
def failure_message | ||
if !@error_subscriber.events.empty? && [email protected]? | ||
event_context = @error_subscriber.events.last.attributes[: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.
Won’t it be confusing to use last
when several errors were reported?
return "Expected error message to be '#{@expected_message}', but got: #{reported_errors}" | ||
end | ||
else | ||
if @expected_error && !actual_error.is_a?(@expected_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 should be is_a? expected_class, no? Just by looking at the implementation. Or what is the case when they won’t match?
|
||
case @expected_message | ||
when Regexp | ||
error.message&.match(@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.
Cosmetic: is safe nav necessary?
|
||
private | ||
|
||
def error_matches_expectation? |
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 the last condition necessary? We already check if it’s empty?
before calling this method.
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.
A fee minor things and simplifications and it looks good to go.
Let’s leave out matching multiple with chains, severity qualifiers, and all extras for later.
|
||
def matches?(block) | ||
if block.nil? | ||
raise ArgumentError, "this matcher doesn't work with value expectations" |
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.
Does this check have the same effect as defining a method supports_value_expectations?
that returns false? It should be.
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.
RSpec is actually allowing the value expectation to reach the matches?
method, even with supports_value_expectations?
defined as false. This mechanism is NOT preventing the value expectation from reaching matches?
. The explicit check was necessary in my testing! But maybe I missed something?
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.
Ha! Nice catch. The RSpec.deprecate
call somehow didn't fail the build. I'll have a closer look.
The block
is not nil
though for expect(:foo).to reports_error
, so this check is useless in its current implementation.
I suggest removing it, and relying on the supports_value_expectations?
.
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 recall now that we kept in 3.x the possibility to pass a lambda to expect
, or define subject
as a lambda and use is_expected.to
with block matchers. We just print a deprecation message now.
Swallowed deprecation messages are due to #2857
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.
After the aftermentioned fix, with
def supports_value_expectations?
false
end
The following passes:
it "warns when used as a value expectation" do
expect {
expect(Rails.error.report(StandardError.new("test error"))).to have_reported_error
}.to raise_error(/implicit block expectation.+deprecated/)
end
We can change the code when that swallowed deprecation fix is merged.
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 can be removed now and any test marked as pending with the reason instead.
def error_message_matches?(error) | ||
return true if @expected_message.nil? | ||
|
||
case @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.
There should be existing examples amongst our matchers of something like value_matches?
that would also allow the use of argument matchers like a_string_including("network")
.
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.
On a second thought, this may make matching more complicated, as if just one argument is passed, and it’s an argument matcher, what should we match it against, the exception itself, or the exception’s message?
This makes it more compelling to build the with_message
qualifier. But it then limits the argument to the matcher to only accept the class. But this is what the Rails’ assertion does anyway.
end | ||
|
||
def self.process_with_context | ||
Rails.error.report(ArgumentError.new("Invalid input"), context: { context: "user_processing", severity: :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.
Nested context here, too. Rename inner to topic:
or :section
?
end | ||
|
||
def self.process_with_context | ||
Rails.error.report(ArgumentError.new("Invalid input"), context: { context: "user_processing", severity: :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.
The severity:
here is inside the context:
, is this correct?
expect { | ||
# Safe operation that doesn't report errors | ||
"safe code" | ||
}.not_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 wonder if the negated form should warn if qualifiers were passed to avoid mistakenly letting unmatched reports through:
expect {
User.process_custom_error
}.not_to have_reported_error("Email is TYPO")
Meaning that if the matcher is used in the negated form, no class, message, or attributes qualifiers are acceptable.
Ping |
Sorry had a small vacation with relatives and had to format my laptop! I'll be pushing improvements soon.. |
features/matchers/README.md
Outdated
# passes when error is reported with specific context attributes | ||
expect { Rails.error.report(StandardError.new, context: { user_id: 123 }) }.to have_reported_error.with_context(user_id: 123) | ||
|
||
|
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.
Remove these two blank lines, I'd do it in a suggestion but Github isn't co-operating.
log/development.log
Outdated
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.
Don't commit this.
end | ||
|
||
def and(_) | ||
raise ArgumentError, "Chaining is not supported" |
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.
Why? If you include the composition helper this should just work ™ ?
This is looking really good, I think its mostly just little things left? |
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