Skip to content

Commit

Permalink
Merge pull request rails#53065 from larouxn/add_active_support_notifi…
Browse files Browse the repository at this point in the history
…cations_event_assertions

Add `ActiveSupport::Testing::NotificationAssertions` test helper module
  • Loading branch information
eileencodes authored Nov 15, 2024
2 parents ed85ce1 + 5cfe5e6 commit c57da03
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 0 deletions.
4 changes: 4 additions & 0 deletions activesupport/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* Add `ActiveSupport::Testing::NotificationAssertions` module to help with testing `ActiveSupport::Notifications`.

*Nicholas La Roux*, *Yishu See*, *Sean Doyle*

* `ActiveSupport::CurrentAttributes#attributes` now will return a new hash object on each call.

Previously, the same hash object was returned each time that method was called.
Expand Down
2 changes: 2 additions & 0 deletions activesupport/lib/active_support/test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require "active_support/testing/file_fixtures"
require "active_support/testing/parallelization"
require "active_support/testing/parallelize_executor"
require "active_support/testing/notification_assertions"
require "concurrent/utility/processor_counter"

module ActiveSupport
Expand Down Expand Up @@ -150,6 +151,7 @@ def parallelize_teardown(&block)
prepend ActiveSupport::Testing::TestsWithoutAssertions
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::ErrorReporterAssertions
include ActiveSupport::Testing::NotificationAssertions
include ActiveSupport::Testing::Deprecation
include ActiveSupport::Testing::ConstantStubbing
include ActiveSupport::Testing::TimeHelpers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

# typed: true
# frozen_string_literal: true

module ActiveSupport
module Testing
module NotificationAssertions
# Assert a notification was emitted with a given +pattern+ and optional +payload+.
#
# You can assert that a notification was emitted by passing a pattern, which accepts
# either a string or regexp, an optional payload, and a block. While the block
# is executed, if a matching notification is emitted, the assertion will pass.
#
# assert_notification("post.submitted", title: "Cool Post") do
# post.submit(title: "Cool Post") # => emits matching notification
# end
#
def assert_notification(pattern, payload = nil, &block)
notifications = capture_notifications(pattern, &block)
assert_not_empty(notifications, "No #{pattern} notifications were found")

return if payload.nil?

notification = notifications.find { |notification| notification.payload == payload }
assert_not_nil(notification, "No #{pattern} notification with payload #{payload} was found")
end

# Assert the number of notifications emitted with a given +pattern+.
#
# You can assert the number of notifications emitted by passing a pattern, which accepts
# either a string or regexp, a count, and a block. While the block is executed,
# the number of matching notifications emitted will be counted. After the block's
# execution completes, the assertion will pass if the count matches.
#
# assert_notifications_count("post.submitted", 1) do
# post.submit(title: "Cool Post") # => emits matching notification
# end
#
def assert_notifications_count(pattern, count, &block)
actual_count = capture_notifications(pattern, &block).count
assert_equal(count, actual_count, "Expected #{count} instead of #{actual_count} notifications for #{pattern}")
end

# Assert no notifications were emitted for a given +pattern+.
#
# You can assert no notifications were emitted by passing a pattern, which accepts
# either a string or regexp, and a block. While the block is executed, if no
# matching notifications are emitted, the assertion will pass.
#
# assert_no_notifications("post.submitted") do
# post.destroy # => emits non-matching notification
# end
#
def assert_no_notifications(pattern = nil, &block)
notifications = capture_notifications(pattern, &block)
error_message = if pattern
"Expected no notifications for #{pattern} but found #{notifications.size}"
else
"Expected no notifications but found #{notifications.size}"
end
assert_empty(notifications, error_message)
end

# Capture emitted notifications, optionally filtered by a +pattern+.
#
# You can capture emitted notifications, optionally filtered by a pattern,
# which accepts either a string or regexp, and a block.
#
# notifications = capture_notifications("post.submitted") do
# post.submit(title: "Cool Post") # => emits matching notification
# end
#
def capture_notifications(pattern = nil, &block)
notifications = []
ActiveSupport::Notifications.subscribed(->(n) { notifications << n }, pattern, &block)
notifications
end
end
end
end
100 changes: 100 additions & 0 deletions activesupport/test/testing/notification_assertions_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require_relative "../abstract_unit"
require "active_support/testing/notification_assertions"

module ActiveSupport
module Testing
class NotificationAssertionsTest < ActiveSupport::TestCase
include NotificationAssertions

def test_assert_notification
assert_notification("post.submitted", title: "Cool Post") do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_notification("post.submitted") do # payload omitted
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_raises(Minitest::Assertion, match: /No post.submitted notifications were found/) do
assert_notification("post.submitted", title: "Cool Post") { nil } # no notifications
end

match = if RUBY_VERSION >= "3.4"
/No post.submitted notification with payload {title: "Cool Post"} was found/
else
/No post.submitted notification with payload {:title=>"Cool Post"} was found/
end
assert_raises(Minitest::Assertion, match:) do
assert_notification("post.submitted", title: "Cool Post") do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cooler Post")
end
end
end

def test_assert_notifications_count
assert_notifications_count("post.submitted", 1) do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_raises(Minitest::Assertion, match: /Expected 1 instead of 2 notifications for post.submitted/) do
assert_notifications_count("post.submitted", 1) do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
ActiveSupport::Notifications.instrument("post.submitted", title: "Cooler Post")
end
end

assert_raises(Minitest::Assertion, match: /Expected 1 instead of 0 notifications for post.submitted/) do
assert_notifications_count("post.submitted", 1) { nil } # no notifications
end
end

def test_assert_no_notifications
assert_no_notifications("post.submitted") { nil } # no notifications

assert_raises(Minitest::Assertion, match: /Expected no notifications for post.submitted but found 1/) do
assert_no_notifications("post.submitted") do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end
end

assert_raises(Minitest::Assertion, match: /Expected no notifications but found 1/) do
assert_no_notifications do
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end
end
end

def test_capture_notifications
notifications = capture_notifications("post.submitted") do # string pattern
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_equal(1, notifications.size)
assert_equal("post.submitted", notifications.first.name)
assert_equal({ title: "Cool Post" }, notifications.first.payload)

notifications = capture_notifications(/post\./) do # regexp pattern
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_equal(1, notifications.size)
assert_equal("post.submitted", notifications.first.name)
assert_equal({ title: "Cool Post" }, notifications.first.payload)

notifications = capture_notifications do # no pattern
ActiveSupport::Notifications.instrument("post.submitted", title: "Cool Post")
end

assert_equal(1, notifications.size)
assert_equal("post.submitted", notifications.first.name)
assert_equal({ title: "Cool Post" }, notifications.first.payload)

notifications = capture_notifications("post.submitted") { nil } # no notifications

assert_empty(notifications)
end
end
end
end

0 comments on commit c57da03

Please sign in to comment.