Skip to content

Commit

Permalink
RSpec matchers (#2424)
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic authored Oct 10, 2024
1 parent 9d3a7be commit 499cbac
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Features

- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424)

## 5.21.0

### Features
Expand Down
91 changes: 91 additions & 0 deletions sentry-ruby/lib/sentry/rspec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts|
match do |sentry_events|
@expected_exception = expected_exception(**opts)
@context = context(**opts)
@tags = tags(**opts)

@expected_event = expected_event(event_message)
@matched_event = find_matched_event(event_message, sentry_events)

return false unless @matched_event

[verify_context(), verify_tags()].all?
end

chain :with_context do |context|
@context = context
end

chain :with_tags do |tags|
@tags = tags
end

failure_message do |sentry_events|
info = ["Failed to find event matching:\n"]
info << " message: #{@expected_event.message.inspect}"
info << " exception: #{@expected_exception.inspect}"
info << " context: #{@context.inspect}"
info << " tags: #{@tags.inspect}"
info << "\n"
info << "Captured events:\n"
info << dump_events(sentry_events)
info.join("\n")
end

def expected_event(event_message)
if @expected_exception
Sentry.get_current_client.event_from_exception(@expected_exception)
else
Sentry.get_current_client.event_from_message(event_message)
end
end

def expected_exception(**opts)
opts[:exception].new(opts[:message]) if opts[:exception]
end

def context(**opts)
opts.fetch(:context, @context || {})
end

def tags(**opts)
opts.fetch(:tags, @tags || {})
end

def find_matched_event(event_message, sentry_events)
@matched_event ||= sentry_events
.find { |event|
if @expected_exception
# Is it OK that we only compare the first exception?
event_exception = event.exception.values.first
expected_event_exception = @expected_event.exception.values.first

event_exception.type == expected_event_exception.type && event_exception.value == expected_event_exception.value
else
event.message == @expected_event.message
end
}
end

def dump_events(sentry_events)
sentry_events.map(&Kernel.method(:Hash)).map do |hash|
hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) }
end.map do |hash|
JSON.pretty_generate(hash)
end.join("\n\n")
end

def verify_context
return true if @context.empty?

@matched_event.contexts.any? { |key, value| value == @context[key] }
end

def verify_tags
return true if @tags.empty?

@tags.all? { |key, value| @matched_event.tags.include?(key) && @matched_event.tags[key] == value }
end
end
123 changes: 123 additions & 0 deletions sentry-ruby/spec/sentry/rspec/matchers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

require "spec_helper"
require "sentry/rspec"

RSpec.describe "Sentry RSpec Matchers" do
include Sentry::TestHelper

before do
# simulate normal user setup
Sentry.init do |config|
config.dsn = 'https://[email protected]/5434472'
config.enabled_environments = ["production"]
config.environment = :test
end

setup_sentry_test
end

after do
teardown_sentry_test
end

let(:exception) { StandardError.new("Gaah!") }

describe "include_sentry_event" do
it "matches events with the given message" do
Sentry.capture_message("Ooops")

expect(sentry_events).to include_sentry_event("Ooops")
end

it "does not match events with a different message" do
Sentry.capture_message("Ooops")

expect(sentry_events).not_to include_sentry_event("Different message")
end

it "matches events with exception" do
Sentry.capture_exception(exception)

expect(sentry_events).to include_sentry_event(exception: exception.class, message: exception.message)
end

it "does not match events with different exception" do
exception = StandardError.new("Gaah!")

Sentry.capture_exception(exception)

expect(sentry_events).not_to include_sentry_event(exception: StandardError, message: "Oops!")
end

it "matches events with context" do
Sentry.set_context("rails.error", { some: "stuff" })
Sentry.capture_message("Ooops")

expect(sentry_events).to include_sentry_event("Ooops")
.with_context("rails.error" => { some: "stuff" })
end

it "does not match events with different context" do
Sentry.set_context("rails.error", { some: "stuff" })
Sentry.capture_message("Ooops")

expect(sentry_events).not_to include_sentry_event("Ooops")
.with_context("rails.error" => { other: "data" })
end

it "matches events with tags" do
Sentry.set_tags(foo: "bar", baz: "qux")
Sentry.capture_message("Ooops")

expect(sentry_events).to include_sentry_event("Ooops")
.with_tags({ foo: "bar", baz: "qux" })
end

it "does not match events with missing tags" do
Sentry.set_tags(foo: "bar")
Sentry.capture_message("Ooops")

expect(sentry_events).not_to include_sentry_event("Ooops")
.with_tags({ foo: "bar", baz: "qux" })
end

it "matches error events with tags and context" do
Sentry.set_tags(foo: "bar", baz: "qux")
Sentry.set_context("rails.error", { some: "stuff" })

Sentry.capture_exception(exception)

expect(sentry_events).to include_sentry_event(exception: exception.class, message: exception.message)
.with_tags({ foo: "bar", baz: "qux" })
.with_context("rails.error" => { some: "stuff" })
end

it "matches error events with tags and context provided as arguments" do
Sentry.set_tags(foo: "bar", baz: "qux")
Sentry.set_context("rails.error", { some: "stuff" })

Sentry.capture_exception(exception)

expect(sentry_events).to include_sentry_event(
exception: exception.class,
message: exception.message,
tags: { foo: "bar", baz: "qux" },
context: { "rails.error" => { some: "stuff" } }
)
end

it "produces a useful failure message" do
Sentry.capture_message("Actual message")

expect {
expect(sentry_events).to include_sentry_event("Expected message")
}.to raise_error(RSpec::Expectations::ExpectationNotMetError) do |error|
expect(error.message).to include("Failed to find event matching:")
expect(error.message).to include("message: \"Expected message\"")
expect(error.message).to include("Captured events:")
expect(error.message).to include("\"message\": \"Actual message\"")
end
end
end
end

0 comments on commit 499cbac

Please sign in to comment.