I was recently working on some example code to demonstrate amqp and got frustrated with the impedance mismatch betweenEventMachine’s asynchronous nature and rspec’s assumption of synchronous test code. I had something seemingly working, but it was flaky and would fail examples seemingly at random. I got some nice tips from the mailing list pointing me to em-spec, an async-enabled BDD library. The Rspec support was initially pretty lacking, but I was able to submit some patches to get it usable. Since then I’ve made a bunch of improvements on my own fork. Hopefully these patches will get pushed upstream soon. Until then, if you want to follow along with the examples below, be sure to grab my version.
This Ain’t (Exactly) Yer Dad’s Rspec
…but it’s close. There are a few differences:
- Your examples magically run inside an EM.run block. Hooray! Unfortunately, so do your before() and after() blocks. We’ll see the consequences below.
- Your examples are run in a Fiber. On pre-1.9 Rubies, fibers are emulated with Threads.
- You’ll need to run the #done method at the end of your test to tell the fiber that your example is done with it.
Getting Started
Let’s begin with a simple example, taken from em-spec’s own specs.
describe EventMachine do
include EM::Spec
it 'should have timers' do
start = Time.now
EM.add_timer(0.5){
(Time.now-start).should be_close( 0.5, 0.1 )
done
}
end
end
To enable the evented behavior, we start by including EMSpec. The first example shows some basic EventMachine behavior, using a timer to schedule code to run at some future time. The first thing to note is that we need to call #done to keep the example from running its fiber forever. Also, note that both our expectation (#should) and #done are called inside the add_timer block. When testing evented code, you’ll find that this is the easiest and most natural way to go. If we had instead placed the call to #done outside of the add_timer block, we would stop execution of the example before the expectation runs:
it 'should not be used like this' do
# Really, this is the wrong way!
start = Time.now
EM.add_timer(0.5){
(Time.now-start).should be_close( 12345, 0.1 ) #never runs!
}
done
end
# Example Passes (OOPS!)
# EventMachine
# - should not be used like this
#
# Finished in 0.004999 seconds
#
# 1 example, 0 failures
The moral of the story is to pay attention to placing your expectations and #done in the right place. If you’re adding tests to existing code (lecture omitted), be sure your tests can fail by putting bogus values in the matchers and running your specs.
All of the other EventMachine behaviors, such as periodic timers and deferrables work similarly:
it 'should have periodic timers' do
num, start = 0, Time.now
timer = EM.add_periodic_timer(0.5){
if (num += 1) == 2
(Time.now-start).should be_close( 1.0, 0.1 )
EM.__send__ :cancel_timer, timer
done
end
}
end
it 'should have deferrables' do
defr = EM::DefaultDeferrable.new
defr.timeout(1)
defr.errback{
done
}
end
Gotchas
Unfortunately, the way that em-spec is monkey patched into Rspec makes your #before() and #after() blocks run inside their own EM-enabled fibers. For example, if you use #before(:each), you need to top off your code with #done:
before(:each) @test_object = Something.new done end
Additionally, each before block will run inside its own personal EM.run block, and the EventMachine reactor is stopped when #done is called. This means that you can set instance variables based on the result of evented code in a before block, but any state set up within the reactor will be lost. This is particularly important when testing AMQP applications.
If this bothers you, or only a few of your expectations use evented behavior, you can use EMSpec as a helper. You’ll have to explicitly wrap evented code inside a block you pass to the em method:
describe EventedCode do
include EM::SpecHelper
it "should do something evented" do
em do
# Your test here
end
end
it "does something not evented" do
# ...
@one_thing.should == another_thing
# No need to call done
end
end
When running evented tests, it’s a good idea to use rspec’s timeout ( -t ) option. This will automatically fail your tests if you botched something and #done never gets called.
AMQP and EMSpec
You can use EMSpec to test applications using Aman Gupta’s amqp library; in fact, that’s why I got started playing with it in the first place. For example, a test using a direct exchange looks like this:
it "should have direct exchanges" do
q = MQ.new.queue("example")
q.publish("hi mom!")
q.subscribe { |msg|
msg.should == "hi mom!"
done
}
end
Once again, placement of the call to done is crucial. Here, we’ve put it in the subscribe block to ensure that it only runs after amqp has received the message.
When testing amqp code, if you have several tests that all use amqp, you may encounter an error message like this: NOT_ALLOWED - attempt to reuse consumer tag 'another-example-635559437038' in AMQP::Protocol::Basic::Consume. I don’t know what this is, but you can work around this issue by using an empty after block:
after(:each) do
done
end
At this point, proponents of mocking and stubbing will certainly be jeering. “Your test code relies on the AMQP server, sacrilege!” they’ll say. I agree, but, as a matter of pragmatism, I’m putting up with this for now.
Go Forth and TATFT!
EMSpec makes testing evented code brain-dead simple. When using it in the helper flavor, you can even use it with cucumber and probably Test::Unit. If you’re developing evented code, you can’t say “we ain’t got no rspec” anymore.
The image at the top by fatllama, CC2 by-cc-sa