TATFT is awesome. AMQP is awesome. If they could be awesome together at the same time, that’s like the dream team. If you can’t get them on the same team, then it’s annoying, like those Kobe and Lebron puppet commercials. The biggest obstacles to creating the dream team, a.k.a. easily testing amqp code, are 1) that amqp objects such as queues and exchanges have relationships to each other that are fairly complicated and don’t lend themselves to simple mocking; 2) the way that AMQP requires a connection to the server (broker) for many of its common operations; and 3) even for simple cases, such as testing code provided to Queue#subscribe, the way amqp shuffles blocks of code around adds a layer of complexity that can be tough to deal with. Given all of this, it’s clear that what’s needed is a way to mock-out the various classes of objects, such Queues, Exchanges, etc., so that they function as normally as possible without requiring a connection to an AMQP broker and give insight into whether or not they’ve received a certain message, whether or not they’ve acknowledged it, and so on.
Moqueue FTW
If you read the title of this post, there won’t be much suspense about what the solution to this problem is. So, without further ado, let me introduce Moqueue, a companion library to amqp (pronounce that Moe-queue, it’s a portmanteau of mocha and queue). Moqueue aims to provide mock objects with an API identical to that provided by real amqp objects, and enough functionality to make code built on amqp run without modification. Moqueue’s test doubles also keep a history of messages received and acknowledged, making it easy to assert that code you’ve built on top of amqp sends and receives the correct messages. Moqueue, is, no surprise, available on github. I’ll make a gem available soon, but for now you gotta use the source. Moqueue doesn’t have any dependencies aside from what’s required by the AMQP library itself. So git yer clone on (git clone git://github.com/danielsdeleo/moqueue.git), and take a quick tour of what Moqueue has to offer.
All the World’s a Nail, but I Only Have This Shotgun
One of Moqueue’s goals is to allow you to mock out amqp at any level. At a very fine level, you can use your favorite mocking library to replace individual queues and exchanges that your application might create. At the course level, Moqueue provides an overload_ampq method, which, as you would expect, overloads methods on AMQP and MQ to return Moqueue’s test doubles instead of real objects. Let’s start there with a little irb exploration:
require "moqueue"
overload_amqp
mq = MQ.new
#=> #<MQ:0x1197ae8>
queue = mq.queue("mocktacular")
#=> #<Moqueue::MockQueue:0x1194550 @name="mocktacular">
topic = mq.topic("lolz")
#=> #<Moqueue::MockExchange:0x11913dc @topic="lolz">
queue.bind(topic, :key=> "cats.*")
#=> #<Moqueue::MockQueue:0x1194550 @name="mocktacular">
queue.subscribe {|header, msg| puts [header.routing_key, msg]}
#=> nil
topic.publish("eatin ur foodz", :key => "cats.inUrFridge")
# cats.inUrFridge
# eatin ur foodz
queue.received_message?("eatin ur foodz")
#=> true
What Just Happened?
This example uses the coarse-grained “mock everything” approach, which we enabled with the #overload_amqp call. Behind the scenes, Moqueue required a file (overloads.rb) that monkey patches the AMQP module and MQ class to use Moqueue’s test doubles for everything. In the next few lines, we see calls to MQ#queue and MQ#topic return a MockQueue and MockExchange. After this, we see the full power of Moqueue. Moqueue has provided us with the ability to bind queues to topic exchanges, just like we do normally. After that, we can publish to the exchange and have the message delivered to the queue, just like normal. On the inside, Moqueue implements the topic exchange routing algorithm, and delivers the message only to the right queues. For example, let’s publish a non-lolcats message:
topic.publish("good grammar is not lolz", :key => "notLolCats")
# queue does not receive message
At the end of the example, we see that the queue has a record of received messages, and allows us to interrogate it about whether it received a particular message. For Test::Unit lovers, it should be pretty apparent that you’re just a assert queue.received_message?("I love T::U!") away from testing your amqp-based code with ease.
I Prefer a Scalpel
If going wild and mocking the whole universe at once isn’t for you, you can use a more surgical approach. The example below is taken right from the spec/examples directory. With some imagination, you should get an idea of how you could use your favorite mocking/stubbing library to replace real Queue and Exchange objects with Moqueue’s test doubles.
require File.dirname(__FILE__) + '/../spec_helper'
describe "AMQP", "when mocked out by Moqueue" do
before(:each) do
reset_broker
end
it "should have direct exchanges" do
queue = mock_queue("direct-exchanges")
queue.publish("you are correct, sir!")
queue.subscribe { |message| "do something with message" }
queue.received_message?("you are correct, sir!").should be_true
end
it "should have direct exchanges with acks" do
queue = mock_queue("direct-with-acks")
queue.publish("yessir!")
queue.subscribe(:ack => true) { |headers, message| headers.ack }
queue.received_ack_for_message?("yessir!").should be_true
end
it "should have topic exchanges" do
topic = mock_exchange(:topic => "TATFT")
queue = mock_queue("rspec-fiend")
queue.bind(topic, :key => "bdd.*").subscribe { |msg| "do something" }
topic.publish("TATFT FTW", :key=> "bdd.4life")
queue.received_message?("TATFT FTW").should be_true
end
it "should have topic exchanges with acks" do
topic = mock_exchange(:topic => "animals")
queue = mock_queue("cat-lover")
queue.bind(topic, :key => "cats.#").subscribe(:ack => true) do |header, msg|
header.ack
"do something with message"
end
topic.publish("OMG kittehs!", :key => "cats.lolz.kittehs")
topic.received_ack_for_message?("OMG kittehs!").should be_true
end
end
The Inevitable Safety Lecture, Part 2358: The Dangers of Mocks
One thing that will be pretty obvious if you’ve used amqp much is that the above examples worked without using EventMachine. For the most part, this is a win, as it makes tests much simpler. The downside, which should be fairly obvious, is that this makes it possible for all of your tests to pass even if your code doesn’t actually run. To mitigate this danger, make sure you run higher-level tests without any mocking. You probably already know that cucumber is awesome for this.
Where It’s At
What it Does Well
As we have seen, Moqueue supports topic exchanges and direct exchanges. Acknowledgements are also supported. With topic exchanges, there’s a small gotcha in that you must call Queue#subscribe before publishing in order for the acknowledgement to be propagated back to the exchange. Direct exchanges are able to record acks regardless of the order of publishing/subscribing. Moqueue has a MockBroker class that keeps references to topic exchanges and queues, ensuring that you get the exact same object if you create a queue with the same name twice or if you create an exchange with the same topic twice. For example:
mock_queue("wibble").object_id == mock_queue("wibble").object_id
# => true
mock_exchange(:topic=>"ruby").object_id == mock_exchange(:topic=>"ruby").object_id
# => true
Although tremendously helpful, this can create dependencies between separate tests if you’re not careful. To avoid this, just add a call to reset_broker in your before(:each) (or equivalent) setup code.
What it May Never Do
Between the intricacies of EventMachine, the way AMQP brokers in general (and RabbitMQ in particular) work, and other factors, there are some particular behaviors you should be aware of that Moqueue does not and might not ever emulate. The first of these is the way that EventMachine schedules message publishing. In my last post, I spent some time showing how EventMachine can build up messages without being able to publish them if you call publish within a loop or iterator. Moqueue is blissfully unaware of EventMachine for the most part (it starts up the EM reactor in the overloaded version of AMQP.start, but that’s all), so calls to MockExchange#publish and MockQueue#publish are blocking–messages are published exactly when you call #publish.
Another behavior that Moqueue does not correctly emulate is the way that real AMQP brokers handle connections and acknowledgments. The amqp library, for instance, requires a graceful shutdown when using acknowledgements in order to correctly notify the server of pending acks. Moqueue doesn’t even have a proper concept of a connection, and handles all acknowledgements immediately, so it cannot mimic the real behavior in this case.
Finally, one of the most commonly discussed issues on the amqp list is the behavior when a client subscribes to a queue containing a backlog of messages. This behavior is most commonly noticed when chaining subscribes, i.e., a message is processed in the subscribe block, then published to another queue. This gist is a good example. The actual behavior of amqp in this situation is to process all of the messages from the first queue, then publish the messages to the next queue, and so on. When using RabbitMQ 1.6 (recently released as a beta), this behavior can be modified by using a “prefetch” option to specify the exact number of messages you want to be sent to a queue at one time. Moqueue doesn’t understand any of this. With Moqueue, a backlog of messages will be processed as soon as a subscribe block is given, however, because all of Moqueue’s #publish calls are blocking, the messages will be passed to subsequent subscribe blocks immediately.
The moral of the story here is that Moqueue is at its best when helping you test small isolated chunks of code: you know, unit testing. Even though one of my primary motivations for making Moqueue so elaborate was to help test the subscribe-then-publish case, keep in mind that Moqueue cannot be relied upon to provide an accurate representation of the global behavior of your app when run in production.
What it Should Do, But Doesn’t Yet
Moqueue started as one of those over-a-weekend coding storms. The common feature of software created this way is usually that it’s missing some obvious features. For Moqueue these are:
Rspec Matchers. I want to write queue.should have_received_message("msg txt")
Update: DONE. See it in action. (Scroll down a bit)Support for fanout exchanges. You can probably fake this using MockExchange#attach_queue, but it would be tedious.
Update: DONE. You’ll find examples in spec/examples/basic_usage_spec.rb and spec/examples/logger_spec.rb- Support for RPC exchanges would also be cool.
- Mocha style expectations on queues, such as queue.expects_message("hello, world!")
- Possibly the most glaring deficiency is that Moqueue doesn’t support Queue#pop, only Queue#subscribe.
Of course, comments, compliments, hate mail, and, most importantly bug fixes are welcomed. Got fork?
Happy TATFTing!
