ears 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7786b24328603a3d9b231e06400194e69ab38b00de05ec0b0ee5424f0ec3a7b
4
- data.tar.gz: 386cedcd1ed08364678da108e1c44e4142d5ba915478476b2d95b14afec3eff3
3
+ metadata.gz: 3ffa13eeabb39addd2d5acb749c6edd32515770b357f7ef6cd3f944cc6aae182
4
+ data.tar.gz: 4aa87462141bd49cbc616dfff68e7d5b5bf24274b7a6c0430ca02182e15259c5
5
5
  SHA512:
6
- metadata.gz: 841d1b043e68ebbcf850fab367e770e145cf3519d15e33f613db18c41e8df72d2c33d0337b50ba3c18d866894defe32d4584230d78447192eb2ac71b6dea134f
7
- data.tar.gz: 7a2bb015f0f4175499c07609cc0ab923af8ecd2157df7a6a57c5c1233c68cfc2d5813ac2452c571551efb83658bfbb5642cf4373bb0efde0a6e9cd88a0530b31
6
+ metadata.gz: 45a3dffcf2f868f753fc985657bc24909c1bba97f1e633eb012ed4185c231d50edc98093f529b336f1e743dabe374a5ff6e662116d72f74ad5c166c0d81146ff
7
+ data.tar.gz: 020e285268b69f96269a45e70abdb2287f48ff4346eac060a3dd6c4cebc8c27248f49c045a1c0a0139ea5c8bfc6aafb959634a6c62f945328c9fb1056d71e875
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.1 (2025-09-09)
4
+
5
+ - Add testing abstractions: `Ears::Testing::TestHelper`, `Ears::Testing::MessageCapture`, and `Ears::Testing::PublisherMock`
6
+
3
7
  ## 0.21.0 (2025-09-08)
4
8
 
5
9
  - Introduce Ears::Publisher with thread-safe channel pooling
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ears (0.21.0)
4
+ ears (0.21.1)
5
5
  bunny (>= 2.22.0)
6
6
  connection_pool (~> 2.4)
7
7
  multi_json
@@ -113,7 +113,7 @@ CHECKSUMS
113
113
  connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b
114
114
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
115
115
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
116
- ears (0.21.0)
116
+ ears (0.21.1)
117
117
  json (2.13.2) sha256=02e1f118d434c6b230a64ffa5c8dee07e3ec96244335c392eaed39e1199dbb68
118
118
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
119
119
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
data/README.md CHANGED
@@ -548,6 +548,115 @@ ensure
548
548
  end
549
549
  ```
550
550
 
551
+ ## Testing
552
+
553
+ Ears provides testing helpers to easily test your message publishing without connecting to RabbitMQ.
554
+
555
+ ### Basic Setup
556
+
557
+ Include the test helper in your RSpec tests and mock the exchanges you want to test:
558
+
559
+ ```ruby
560
+ require 'ears/testing'
561
+
562
+ RSpec.describe MyService do
563
+ include Ears::Testing::TestHelper
564
+
565
+ before do
566
+ # Mock exchanges that your code will publish to
567
+ mock_ears('events', 'notifications')
568
+ end
569
+
570
+ after do
571
+ # Clean up mocks and captured messages
572
+ ears_reset!
573
+ end
574
+ end
575
+ ```
576
+
577
+ ### Capturing and Inspecting Messages
578
+
579
+ Use the helper methods to inspect published messages:
580
+
581
+ ```ruby
582
+ it 'publishes user creation event' do
583
+ service = UserService.new
584
+ service.create_user(name: 'John', email: 'john@example.com')
585
+
586
+ # Get all messages published to 'events' exchange
587
+ messages = published_messages('events')
588
+ expect(messages.size).to eq(1)
589
+
590
+ # Inspect the message
591
+ message = messages.first
592
+ expect(message.routing_key).to eq('user.created')
593
+ expect(message.data).to include(name: 'John')
594
+ expect(message.options[:headers]).to include(version: '1.0')
595
+ end
596
+ ```
597
+
598
+ ### Available Helper Methods
599
+
600
+ - `published_messages(exchange_name = nil)` - Get messages for a specific exchange or all messages
601
+ - `last_published_message(exchange_name = nil)` - Get the most recent message
602
+ - `clear_published_messages` - Clear captured messages during a test
603
+
604
+ ### Message Properties
605
+
606
+ Each captured message has the following properties:
607
+
608
+ - `exchange_name` - Name of the exchange
609
+ - `routing_key` - Message routing key
610
+ - `data` - The message payload
611
+ - `options` - Publishing options (headers, persistent, etc.)
612
+ - `timestamp` - When the message was captured
613
+ - `thread_id` - Thread that published the message
614
+
615
+ ### Error Handling
616
+
617
+ By default, publishing to unmocked exchanges raises an error:
618
+
619
+ ```ruby
620
+ it 'raises error for unmocked exchanges' do
621
+ publisher = Ears::Publisher.new('unmocked_exchange')
622
+
623
+ expect {
624
+ publisher.publish({ data: 'test' }, routing_key: 'test')
625
+ }.to raise_error(Ears::Testing::UnmockedExchangeError)
626
+ end
627
+ ```
628
+
629
+ ### Complete Example
630
+
631
+ ```ruby
632
+ require 'ears/testing'
633
+
634
+ RSpec.describe OrderProcessor do
635
+ include Ears::Testing::TestHelper
636
+
637
+ before { mock_ears('events', 'notifications') }
638
+ after { ears_reset! }
639
+
640
+ it 'publishes events when processing order' do
641
+ processor = OrderProcessor.new
642
+ order = { id: 123, items: ['item1'], total: 99.99 }
643
+
644
+ processor.process(order)
645
+
646
+ # Check event was published
647
+ events = published_messages('events')
648
+ expect(events.size).to eq(1)
649
+ expect(events.first.routing_key).to eq('order.processed')
650
+ expect(events.first.data[:order_id]).to eq(123)
651
+
652
+ # Check notification was sent
653
+ notifications = published_messages('notifications')
654
+ expect(notifications.size).to eq(1)
655
+ expect(notifications.first.routing_key).to eq('email.order_confirmation')
656
+ end
657
+ end
658
+ ```
659
+
551
660
  ## Documentation
552
661
 
553
662
  If you need more in-depth information, look at [our API documentation](https://www.rubydoc.info/gems/ears).
@@ -0,0 +1,89 @@
1
+ module Ears
2
+ module Testing
3
+ class MessageCapture
4
+ Message =
5
+ Struct.new(
6
+ :exchange_name,
7
+ :routing_key,
8
+ :data,
9
+ :options,
10
+ :timestamp,
11
+ :thread_id,
12
+ keyword_init: true,
13
+ )
14
+
15
+ def initialize
16
+ @messages = {}
17
+ @mutex = Mutex.new
18
+ end
19
+
20
+ def add_message(exchange_name, data, routing_key, options = {})
21
+ @mutex.synchronize do
22
+ @messages[exchange_name] ||= []
23
+
24
+ message =
25
+ Message.new(
26
+ exchange_name: exchange_name,
27
+ routing_key: routing_key,
28
+ data: data,
29
+ options: options,
30
+ timestamp: Time.now,
31
+ thread_id: Thread.current.object_id.to_s,
32
+ )
33
+
34
+ @messages[exchange_name] << message
35
+
36
+ shift_messages(exchange_name)
37
+
38
+ message
39
+ end
40
+ end
41
+
42
+ def messages_for(exchange_name)
43
+ @mutex.synchronize { (@messages[exchange_name] || []).dup }
44
+ end
45
+
46
+ def all_messages
47
+ @mutex.synchronize { @messages.values.flatten }
48
+ end
49
+
50
+ def clear
51
+ @mutex.synchronize { @messages.clear }
52
+ end
53
+
54
+ def count(exchange_name = nil)
55
+ @mutex.synchronize do
56
+ return (@messages[exchange_name] || []).size if exchange_name
57
+
58
+ @messages.values.sum(&:size)
59
+ end
60
+ end
61
+
62
+ def empty?
63
+ @mutex.synchronize do
64
+ @messages.empty? || @messages.values.all?(&:empty?)
65
+ end
66
+ end
67
+
68
+ def find_messages(exchange_name: nil, routing_key: nil, data: nil)
69
+ messages = exchange_name ? messages_for(exchange_name) : all_messages
70
+
71
+ messages.select do |msg|
72
+ next false if routing_key && msg.routing_key != routing_key
73
+ next false if data && msg.data != data
74
+
75
+ true
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def shift_messages(exchange_name)
82
+ max_messages = Ears::Testing.configuration.max_captured_messages
83
+ if @messages[exchange_name].size > max_messages
84
+ @messages[exchange_name].shift
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,102 @@
1
+ require 'rspec/mocks'
2
+
3
+ module Ears
4
+ module Testing
5
+ class UnmockedExchangeError < StandardError
6
+ end
7
+
8
+ class PublisherMock
9
+ include RSpec::Mocks::ExampleMethods
10
+
11
+ def initialize(exchange_names, message_capture)
12
+ @exchange_names = Array(exchange_names)
13
+ @message_capture = message_capture
14
+ @mock_exchanges = {}
15
+ end
16
+
17
+ def setup_mocks
18
+ setup_connection
19
+ setup_channel_pool
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :exchange_names, :message_capture, :mock_exchanges
25
+
26
+ def setup_connection
27
+ mock_connection = instance_double(Bunny::Session, open?: true)
28
+ Ears.instance_variable_set(:@connection, mock_connection)
29
+ end
30
+
31
+ def setup_channel_pool
32
+ mock_channel = create_mock_channel
33
+ allow(Ears::PublisherChannelPool).to receive(:with_channel).and_yield(
34
+ mock_channel,
35
+ )
36
+ mock_channel
37
+ end
38
+
39
+ def create_mock_channel
40
+ instance_double(Bunny::Channel).tap do |channel|
41
+ setup_exchange_declare(channel)
42
+ setup_register_exchange(channel)
43
+ setup_basic_publish(channel)
44
+ end
45
+ end
46
+
47
+ def setup_exchange_declare(channel)
48
+ allow(channel).to receive(:exchange_declare) do |name, type, options|
49
+ create_or_get_mock_exchange(name, type, options)
50
+ end
51
+ end
52
+
53
+ def setup_register_exchange(channel)
54
+ allow(channel).to receive(:register_exchange)
55
+ end
56
+
57
+ def setup_basic_publish(channel)
58
+ allow(channel).to receive(
59
+ :basic_publish,
60
+ ) do |data, exchange, routing_key, options|
61
+ if exchange_names.include?(exchange)
62
+ capture_message(exchange, data, routing_key, options)
63
+ elsif strict_mocking?
64
+ raise_unmocked_exchange_error(exchange)
65
+ end
66
+ end
67
+ end
68
+
69
+ def create_or_get_mock_exchange(name, type, _options)
70
+ mock_exchanges[name] ||= create_mock_exchange(name, type)
71
+ end
72
+
73
+ def create_mock_exchange(name, type)
74
+ exchange = instance_double(Bunny::Exchange, name: name, type: type)
75
+
76
+ setup_exchange_publish(exchange, name)
77
+
78
+ exchange
79
+ end
80
+
81
+ def setup_exchange_publish(exchange, name)
82
+ allow(exchange).to receive(:publish) do |data, routing_options|
83
+ routing_key = routing_options[:routing_key]
84
+ capture_message(name, data, routing_key, routing_options)
85
+ end
86
+ end
87
+
88
+ def capture_message(exchange_name, data, routing_key, options)
89
+ message_capture.add_message(exchange_name, data, routing_key, options)
90
+ end
91
+
92
+ def strict_mocking?
93
+ Ears::Testing.configuration.strict_exchange_mocking
94
+ end
95
+
96
+ def raise_unmocked_exchange_error(exchange)
97
+ raise UnmockedExchangeError,
98
+ "Exchange '#{exchange}' has not been mocked. Add mock_ears('#{exchange}') to your test setup."
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,50 @@
1
+ require 'rspec/mocks'
2
+
3
+ module Ears
4
+ module Testing
5
+ module TestHelper
6
+ include RSpec::Mocks::ExampleMethods
7
+
8
+ def mock_ears(*exchange_names)
9
+ Ears::Testing.message_capture ||= MessageCapture.new
10
+
11
+ @original_connection = Ears.instance_variable_get(:@connection)
12
+
13
+ publisher_mock =
14
+ PublisherMock.new(exchange_names, Ears::Testing.message_capture)
15
+ publisher_mock.setup_mocks
16
+ end
17
+
18
+ def ears_reset!
19
+ Ears::Testing.message_capture = nil
20
+
21
+ if instance_variable_defined?(:@original_connection)
22
+ Ears.instance_variable_set(:@connection, @original_connection)
23
+ remove_instance_variable(:@original_connection)
24
+ end
25
+
26
+ if defined?(Ears::PublisherChannelPool)
27
+ Ears::PublisherChannelPool.reset!
28
+ end
29
+ end
30
+
31
+ def published_messages(exchange_name = nil)
32
+ return [] unless Ears::Testing.message_capture
33
+
34
+ if exchange_name
35
+ return Ears::Testing.message_capture.messages_for(exchange_name)
36
+ end
37
+
38
+ Ears::Testing.message_capture.all_messages
39
+ end
40
+
41
+ def last_published_message(exchange_name = nil)
42
+ published_messages(exchange_name).last
43
+ end
44
+
45
+ def clear_published_messages
46
+ Ears::Testing.message_capture&.clear
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require 'ears'
2
+ require 'ears/testing/test_helper'
3
+ require 'ears/testing/message_capture'
4
+ require 'ears/testing/publisher_mock'
5
+
6
+ module Ears
7
+ module Testing
8
+ class << self
9
+ attr_accessor :message_capture
10
+
11
+ def configure
12
+ yield(configuration) if block_given?
13
+ end
14
+
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def reset!
20
+ @message_capture = nil
21
+ @configuration = nil
22
+ end
23
+ end
24
+
25
+ class Configuration
26
+ attr_accessor :max_captured_messages,
27
+ :auto_cleanup,
28
+ :strict_exchange_mocking
29
+
30
+ def initialize
31
+ @max_captured_messages = 1000
32
+ @auto_cleanup = true
33
+ @strict_exchange_mocking = true
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/ears/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ears
2
- VERSION = '0.21.0'
2
+ VERSION = '0.21.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ears
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - InVision AG
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-08 00:00:00.000000000 Z
11
+ date: 2025-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -91,6 +91,10 @@ files:
91
91
  - lib/ears/publisher_channel_pool.rb
92
92
  - lib/ears/publisher_retry_handler.rb
93
93
  - lib/ears/setup.rb
94
+ - lib/ears/testing.rb
95
+ - lib/ears/testing/message_capture.rb
96
+ - lib/ears/testing/publisher_mock.rb
97
+ - lib/ears/testing/test_helper.rb
94
98
  - lib/ears/version.rb
95
99
  - package-lock.json
96
100
  - package.json