Testing a Consumer - messagebus/lapine GitHub Wiki

When testing a consumer message hander, there are at least two options:

  • trust Lapine, and unit test without RabbitMQ
  • write integration tests that publish messages to RabbitMQ, then test the end results

For information on testing consumers, see Testing a Publisher

Writing unit tests

Consumer handlers must define a #handle_lapine_payload method, which you can easily test without publishing messages to RabbitMQ.

class MyHandler
  def self.handle_lapine_payload(payload, metadata)
    MyHandlerWorker.perform_async(payload)
  end
end

For this simple class, one could write a stubbed unit test:

RSpec.describe MyHandler do
  describe 'handling lapine messages' do
    before { allow(MyHandlerWorker).to receive(:perform_async) }

    it 'passes off data to MyHandlerWorker' do
      MyHandler.handle_lapine_payload({'omg' => 'lol'}, {})
      expect(MyHandlerWorker).to have_received(:perform_async).with({'omg' => 'lol'})
    end
  end
end

Writing integration tests

When writing full integration tests that pass messages to RabbitMQ, the em-spec gem is useful for using EventMachine in your tests.

require 'spec_helper'
require 'amqp'
require 'em-spec/rspec'
require 'lapine/consumer'

RSpec.describe MyHandler do
  include EM::SpecHelper

  let(:runner) { Lapine::Consumer::Runner.new(argv) }
  let(:argv) { %w(--config config/lapine.yml --transient --logfile /dev/null) }

  before do
    allow(MyHandlerWorker).to receive(:perform_async)
  end

  let(:payload) { {'omg' => 'lol'} }
  let(:json) { payload.to_json }

  it 'passes off data to MyHandlerWorker' do
    em do
      subject.run
      EventMachine.add_timer(1.0) {
        conn = Lapine::Consumer::Connection.new(runner.config, 'efrafa.topic')
        conn.exchange.publish(json, routing_key: 'omg.lol')
      }
      EventMachine.add_timer(2.0) do
        consumer.topology.close!
        done
      end
    end
    expect(MyHandlerWorker).to have_received(:perform_async).with(payload)
  end
end

Take note of a few things:

Messages are published inside of an EventMachine timer. This is because inside the runner, queues are defined inside an EventMachine.run block. If the messages are published in-line, there is a race condition where messages may be published before queues are registered. When this race condition is triggered, RabbitMQ will accept the message, see that there are no queues in which to write it, then throw it away. By publishing in a timer, we are waiting for the queues to be registered before we publish the message.

We stop EventMachine after a few seconds. Otherwise the run loop will never exit. Because of the potential for race conditions between the test thread and the runner's run loop, we need to wait for a while to ensure that the runner wakes up and does its work.

We configure the runner to throw away log messages. By default, the runner logs to STDOUT. It can be extremely annoying when unnecessary log data clutters up your test runs, so we tell Lapine to throw away log messages.

We pass --transient to the runner. This tells Lapine that any queues registered should be done so with auto_delete: true. Otherwise queues will persist after the test completes.

We do not register a vhost or an exchange. That can be done outside of the test block, probably in a rake reset task or something like it.