Skip to content

WIP: Introduce have_reported_error matcher - #2849

Open
skatkov wants to merge 36 commits into
rspec:mainfrom
skatkov:have-reported-error
Open

skatkov wants to merge 36 commits into
rspec:mainfrom
skatkov:have-reported-error

Conversation

@skatkov

@skatkov skatkov commented Jun 4, 2025

Copy link
Copy Markdown

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

@example Checking for any error
  expect { Rails.error.report(StandardError.new) }.to have_reported_error

@example Checking for specific error class
  expect { Rails.error.report(MyError.new) }.to have_reported_error(MyError)

@example Checking for specific error instance with message
  expect { Rails.error.report(MyError.new("message")) }.to have_reported_error(MyError.new("message"))

@example Checking error attributes
  expect { Rails.error.report(StandardError.new, context: "test") }.to have_reported_error.with_context(section: "test")

@example Checking error message patterns
  expect { Rails.error.report(StandardError.new("test message")) }.to have_reported_error(/test/)

@example Negation
  expect { "safe code" }.not_to have_reported_error

TODO

Outline of things that we want to do before marking this as completed.

@skatkov
skatkov force-pushed the have-reported-error branch from 8b837a8 to d2d0e51 Compare June 7, 2025 20:44

@pirj pirj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread features/matchers/README.md
Comment thread features/matchers/README.md
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
@skatkov
skatkov force-pushed the have-reported-error branch 3 times, most recently from 71685ad to 6135d5d Compare June 22, 2025 20:59
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread features/matchers/README.md Outdated
Comment thread features/matchers/have_reported_error_matcher.feature Outdated
Comment thread features/matchers/have_reported_error_matcher.feature
Comment thread features/matchers/have_reported_error_matcher.feature Outdated
Comment thread features/matchers/have_reported_error_matcher.feature Outdated
Comment on lines +183 to +218
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

Copy link
Copy Markdown
Member

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.

Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
@JonRowe

JonRowe commented Jun 25, 2025

Copy link
Copy Markdown
Member

👋 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.

@skatkov

skatkov commented Jun 27, 2025

Copy link
Copy Markdown
Author

@JonRowe thanks for review.

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.

Which of these you would rather go with?
.have_reported_error(StandardError, /wrong attributes/).with(param1: 'test')

or

.have_reported_error(StandardError).with(message:/wrong attributes/, params1: 'test1')

??

I assume, that first option is prefered to keep similarity with raised_error matcher.
https://rspec.info/features/3-13/rspec-expectations/built-in-matchers/raise-error/

@JonRowe

JonRowe commented Jun 27, 2025

Copy link
Copy Markdown
Member

Given that you already have with, I think:

.have_reported_error(StandardError).with(/wrong attributes/, params1: 'test1')

But if you insisted on message: in with I could be persuaded

@pirj

pirj commented Jun 27, 2025

Copy link
Copy Markdown
Member

It makes sense to separate matching of args passed to report and exception’s own args, including the message. Both can receive args, right? It would open a way for ambiguity and confusion.
raise_error accepts have_attributes as an argument matcher. It supports with_message. Wdyt if we reserve with for additional args passed to report, but keep it separate from with_message?

@skatkov

skatkov commented Jun 29, 2025

Copy link
Copy Markdown
Author

It makes sense to separate matching of args passed to report and exception’s own args, including the message. Both can receive args, right? It would open a way for ambiguity and confusion.

The only argument that the exception class accepts is "message". We can of course subclass it and support way more than that, but I'm not sure if we should support this in a matcher?

I have a feeling, that having a matcher interface similar to raise_error would make it simpler for people to memorize it. I don't mind changing .with to .have_attributes as well.

@JonRowe

JonRowe commented Jun 30, 2025

Copy link
Copy Markdown
Member

I hadn't clocked that the extra attributes were in addition to the error, I think matching the raise_error interface in terms of report_error(ErrorClass, message_or_regexp) makes sense with the existing with becoming something clearer, with_metadata? What does Rails call the extra data?

@skatkov

skatkov commented Jun 30, 2025

Copy link
Copy Markdown
Author

Here is a source code for Rails.error.report.

https://github.com/rails/rails/blob/36601bbb02bc3570f5609db2127c77afca575d6c/activesupport/lib/active_support/error_reporter.rb#L233C23-L233C112

These attributes in rails case have a name "context". so with_context might be more appropriate.

@skatkov
skatkov force-pushed the have-reported-error branch 4 times, most recently from f90fc3b to 2488a71 Compare July 1, 2025 23:28
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
end

def failure_message
if !@error_subscriber.events.empty? && !@attributes.empty?

Copy link
Copy Markdown
Member

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? && !@attributes.empty?
event_context = @error_subscriber.events.last.attributes[:context]

Copy link
Copy Markdown
Member

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)

Copy link
Copy Markdown
Member

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?

Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated
Comment thread spec/rspec/rails/matchers/have_reported_error_spec.rb Outdated

private

def error_matches_expectation?

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular method checks that the error matches one that we expect, not just that the error occurred.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we check if @reports.empty? and return.

Right after the check, we call error_matches_expectation?.

The last condition check @reports.count.positive? here is redundant.

@pirj pirj left a comment

Copy link
Copy Markdown
Member

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.

Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
Comment thread lib/rspec/rails/matchers/have_reported_error.rb
Comment thread lib/rspec/rails/matchers/have_reported_error.rb Outdated
#
# @param expected_error_or_message [Class, String, Regexp, nil] the expected error class, message string, or message pattern
# @param expected_message [String, Regexp, nil] the expected error message to match
def have_reported_error(expected_error_or_message = UndefinedValue, expected_message = nil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have default attributes both here and in the initializer. Worth leaving just here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still defaults both here and there:

def initialize(expected_error_or_message = UndefinedValue, expected_message = nil)

Let's remove there.

end

def self.process_with_context
Rails.error.report(ArgumentError.new("Invalid input"), context: { context: "user_processing", severity: :error })

Copy link
Copy Markdown
Member

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?

Comment thread features/matchers/have_reported_error_matcher.feature Outdated
skatkov and others added 27 commits September 24, 2025 13:13
Commit grammar improvements

Co-authored-by: Jon Rowe <mail@jonrowe.co.uk>
Co-authored-by: Phil Pirozhkov <pirj@users.noreply.github.com>
@skatkov
skatkov force-pushed the have-reported-error branch from b5568a0 to aa4c84d Compare September 24, 2025 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Rails 7.1 error reporter

3 participants