-
Notifications
You must be signed in to change notification settings - Fork 0
Testing Guide
This guide shows how to test producers and consumers without RabbitMQ by using Lepus' testing helpers and RSpec matchers.
- Overview
- Setup (RSpec and Minitest)
- Producer testing
- Quick start
- Matchers
- Inspecting exchanges and messages
- Consumer testing
- Quick start
- Building messages with
MessageBuilder - Matchers
- Running a consumer directly
- API reference
Lepus::TestingLepus::Testing::ExchangeLepus::Testing::MessageBuilder- Message structure stored in fake exchanges
- RSpec matchers
When testing, enable the fake publisher so that producers do not hit RabbitMQ. Published messages are stored in in-memory fake exchanges that you can assert against.
Add the testing helpers and enable producers for your test suite.
RSpec (spec/spec_helper.rb):
require 'lepus/testing'
RSpec.configure do |config|
config.before do
::Lepus::Testing.enable!
::Lepus::Testing.clear_all_messages!
end
config.before do
::Lepus::Testing.disable!
end
endMinitest (e.g., test_helper.rb):
require "lepus/testing"
class ActiveSupport::TestCase
setup do
Lepus::Testing.enable!
Lepus::Testing.clear_all_messages!
end
teardown do
Lepus::Testing.disable!
end
endrequire "lepus/testing"
Lepus::Testing.fake_publisher! # .enable! also activate fake publisher
MyProducer.publish({user_id: 1}, routing_key: "users.created")
expect(Lepus::Testing::Exchange["users"].size).to eq(1)
expect(MyProducer.messages.size).to eq(1) # shortcut per producer# Any message
expect { MyProducer.publish({}) }.to lepus_publish_message
expect(MyProducer).to lepus_publish_message
# Exact count
expect {
MyProducer.publish("one")
MyProducer.publish("two")
}.to lepus_publish_message(2)
# Filter by exchange
expect { MyProducer.publish({}) }.to lepus_publish_message.to_exchange("users")
# Filter by routing key
expect { MyProducer.publish({}, routing_key: "user.created") }
.to lepus_publish_message.with_routing_key("user.created")
# Filter by payload using RSpec matchers
expect { MyProducer.publish({user_id: 123}) }
.to lepus_publish_message.with(a_hash_including(user_id: 123))
# Combined
expect { MyProducer.publish({user_id: 123}, routing_key: "user.created") }
.to lepus_publish_message
.to_exchange("users")
.with_routing_key("user.created")
.with(a_hash_including(user_id: 123))
# Negative
expect { }.not_to lepus_publish_messageNotes:
- Producers expose
MyProducer.messagesin testing to fetch messages for their exchange. - Publishing respects
Lepus::Producers.exchange_enabled?(exchange_name). If an exchange is disabled, nothing is stored.
ex = Lepus::Testing::Exchange["users"]
ex.messages # => Array<Hash>
ex.find_messages(routing_key: "user.created")
ex.clear_messages
Lepus::Testing.exchanges # => {"users"=>#<Exchange ...>, ...}
Lepus::Testing.clear_all_messages!Lepus lets you execute Lepus::Consumer classes synchronously with realistic Lepus::Message instances.
require "lepus/testing"
Lepus::Testing.consumer_raise_errors! # allow specs to observe errors instead of swallowing them, the `.enable!` can also be used instead
result = Lepus::Testing.consumer_perform(MyConsumer, {"action" => "create"})
# => :ack | :reject | :requeue | :nackBehavior when passing input to consumer_perform:
-
Hash: treated as payload,content_typeset toapplication/json. -
String: treated as payload,content_typeset totext/plain. -
Lepus::Message: used as-is.
message = Lepus::Testing.message_builder
.with_payload({"user_id" => 123})
.with_routing_key("users.create")
.with_exchange("users")
.with_delivery_tag(42)
.with_redelivered(true)
.with_headers({"x-retry-count" => 2})
.with_content_type("application/json")
.build
result = Lepus::Testing.consumer_perform(MyConsumer, message)You can assert the result a consumer returns when processing a message.
Shorthand (consumer on the left, message as an argument to the matcher):
expect(MyConsumer).to lepus_acknowledge_message({"action" => "create"})
expect(MyConsumer).to lepus_reject_message({"action" => "reject"})
expect(MyConsumer).to lepus_requeue_message({"action" => "requeue"})
expect(MyConsumer).to lepus_nack_message({"action" => "nack"})Explicit chaining (use when you prefer to set the message separately):
message = Lepus::Testing.message_builder
.with_payload("hello")
.with_content_type("text/plain")
.build
expect(MyConsumer)
.to lepus_acknowledge_message
.with_message(message)Tip: Silence error logs in a specific example
allow(Lepus).to receive(:logger).and_return(double("logger", error: nil))# Hash payload
result = Lepus::Testing.consumer_perform(MyConsumer, {"action" => "create"})
# String payload
result = Lepus::Testing.consumer_perform(MyConsumer, "raw string")
# Pre-built message
message = Lepus::Testing.message_builder
.with_payload({"action" => "create"})
.with_content_type("application/json")
.build
result = Lepus::Testing.consumer_perform(MyConsumer, message)-
enable!Enable fake publishing mode and consumer error re-raising. -
disable!Disable fake publishing mode and consumer error re-raising -
fake_publisher!Enable fake publishing mode. -
fake_publisher_disable!Disable fake publishing mode. -
fake_publisher_enabled?Check if fake publishing is enabled. -
consumer_raise_errors!When enabled, consumer exceptions are re-raised instead of being converted to :reject -
consumer_capture_errors!Disable consumer error re-raising (default behavior converts to :reject) -
consumer_raise_errors?Check if consumer errors should be re-raised in tests. -
clear_all_messages!Clear all messages from all fake exchanges. -
exchangesReturn all fake exchanges as aHash<String, Exchange>. -
exchange(name)Get or create a fake exchange byname. -
producer_messages(producer_class)Return messages for a producer's exchange. Also available viaProducer.messagesin tests. -
consumer_perform(consumer_class, message_or_payload)Build a message if needed and callprocess_delivery. Returns one of:ack,:reject,:requeue,:nack. -
message_builderReturn a newLepus::Testing::MessageBuilder.
Instance methods:
-
add_message(message)Add a message to the exchange. -
clear_messagesRemove all messages from the exchange. -
sizeNumber of stored messages. -
empty?Whether there are any messages. -
find_messages(criteria = {})Filter by keys like:routing_key,:payload,:headers, etc. -
messagesArray of stored messages.
Class methods:
-
Exchange[name]Get or create an exchange by name. -
Exchange.allHash of all exchanges. -
Exchange.clear_all_messages!Clear messages from all exchanges. -
Exchange.clear_all!Remove all exchanges. -
Exchange.total_messagesCount across all exchanges.
Fluent builder for realistic Lepus::Message instances:
- Payload:
with_payload(value). - Delivery info:
with_delivery_tag,with_routing_key,with_exchange,with_consumer_tag,with_redelivered,with_delivery_info_attrs(hash). - Metadata:
with_content_type,with_headers,with_correlation_id,with_reply_to,with_expiration,with_message_id,with_timestamp,with_type,with_user_id,with_app_id,with_delivery_mode,with_priority,with_metadata_attrs(hash). -
buildReturns aLepus::Message. RaisesArgumentErrorif payload is missing.
Messages stored via the fake publisher have this shape:
{
exchange: "exchange_name",
payload: "original message passed to publish", # String or Hash
routing_key: "routing.key",
headers: { "key" => "value" },
persistent: true,
mandatory: nil,
immediate: nil,
content_type: (string? "text/plain" : "application/json"),
timestamp: Time.now
}Producer:
-
lepus_publish_message(count = nil)- Chain:
.to_exchange(name),.with_routing_key(key),.with(payload_matcher)
- Chain:
Consumer:
-
lepus_acknowledge_message(message_or_payload = nil)(alias:lepus_ack_message) lepus_reject_message(message_or_payload = nil)lepus_requeue_message(message_or_payload = nil)-
lepus_nack_message(message_or_payload = nil)- Chain:
.with_message(message_or_payload)
- Chain:
Notes:
- Consumer matchers accept either style:
expect(MyConsumer).to lepus_acknowledge_message({..})expect(MyConsumer).to lepus_acknowledge_message.with_message({..})
- Ensure
Lepus::Producers.enable!is called in test setup; disabled exchanges will drop messages. - If you need quiet logs for a failing consumer example, stub the logger as shown above.