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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +2 -2
- data/README.md +109 -0
- data/lib/ears/testing/message_capture.rb +89 -0
- data/lib/ears/testing/publisher_mock.rb +102 -0
- data/lib/ears/testing/test_helper.rb +50 -0
- data/lib/ears/testing.rb +37 -0
- data/lib/ears/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ffa13eeabb39addd2d5acb749c6edd32515770b357f7ef6cd3f944cc6aae182
|
4
|
+
data.tar.gz: 4aa87462141bd49cbc616dfff68e7d5b5bf24274b7a6c0430ca02182e15259c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45a3dffcf2f868f753fc985657bc24909c1bba97f1e633eb012ed4185c231d50edc98093f529b336f1e743dabe374a5ff6e662116d72f74ad5c166c0d81146ff
|
7
|
+
data.tar.gz: 020e285268b69f96269a45e70abdb2287f48ff4346eac060a3dd6c4cebc8c27248f49c045a1c0a0139ea5c8bfc6aafb959634a6c62f945328c9fb1056d71e875
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ears (0.21.
|
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.
|
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
|
data/lib/ears/testing.rb
ADDED
@@ -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
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.
|
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-
|
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
|