Skip to content

Testing Guide

Marcos G. Zimmermann edited this page Sep 25, 2025 · 1 revision

Lepus Testing Guide

This guide shows how to test producers and consumers without RabbitMQ by using Lepus' testing helpers and RSpec matchers.

Contents

  • 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::Testing
    • Lepus::Testing::Exchange
    • Lepus::Testing::MessageBuilder
    • Message structure stored in fake exchanges
    • RSpec matchers

Overview

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.


Setup

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
end

Minitest (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
end

Producer testing

Quick start

require "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

Matchers

# 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_message

Notes:

  • Producers expose MyProducer.messages in testing to fetch messages for their exchange.
  • Publishing respects Lepus::Producers.exchange_enabled?(exchange_name). If an exchange is disabled, nothing is stored.

Inspecting exchanges and messages

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!

Consumer testing

Lepus lets you execute Lepus::Consumer classes synchronously with realistic Lepus::Message instances.

Quick start

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 | :nack

Behavior when passing input to consumer_perform:

  • Hash: treated as payload, content_type set to application/json.
  • String: treated as payload, content_type set to text/plain.
  • Lepus::Message: used as-is.

Building messages with MessageBuilder

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)

Matchers

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))

Running a consumer directly

# 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)

API reference

Lepus::Testing

  • 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.
  • exchanges Return all fake exchanges as a Hash<String, Exchange>.
  • exchange(name) Get or create a fake exchange by name.
  • producer_messages(producer_class) Return messages for a producer's exchange. Also available via Producer.messages in tests.
  • consumer_perform(consumer_class, message_or_payload) Build a message if needed and call process_delivery. Returns one of :ack, :reject, :requeue, :nack.
  • message_builder Return a new Lepus::Testing::MessageBuilder.

Lepus::Testing::Exchange

Instance methods:

  • add_message(message) Add a message to the exchange.
  • clear_messages Remove all messages from the exchange.
  • size Number of stored messages.
  • empty? Whether there are any messages.
  • find_messages(criteria = {}) Filter by keys like :routing_key, :payload, :headers, etc.
  • messages Array of stored messages.

Class methods:

  • Exchange[name] Get or create an exchange by name.
  • Exchange.all Hash of all exchanges.
  • Exchange.clear_all_messages! Clear messages from all exchanges.
  • Exchange.clear_all! Remove all exchanges.
  • Exchange.total_messages Count across all exchanges.

Lepus::Testing::MessageBuilder

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).
  • build Returns a Lepus::Message. Raises ArgumentError if payload is missing.

Message structure in fake exchanges

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
}

RSpec matchers

Producer:

  • lepus_publish_message(count = nil)
    • Chain: .to_exchange(name), .with_routing_key(key), .with(payload_matcher)

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)

Notes:

  • Consumer matchers accept either style:
    • expect(MyConsumer).to lepus_acknowledge_message({..})
    • expect(MyConsumer).to lepus_acknowledge_message.with_message({..})

Troubleshooting

  • 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.

Clone this wiki locally