= amqp-spec
by: Arvicco
url: http://github.com/ruby-amqp/amqp-spec

== Summary

Simple API for writing asynchronous EventMachine/AMQP specs. Supports RSpec and RSpec2.
Should work under MRI 1.8.7, 1.9.2 (and JRuby 1.6.0 - work in progress!)

== Description

EventMachine-based code, including synchronous {AMQP library}[http://github.com/tmm1/amqp]
is notoriously difficult to test. To the point that many people recommend using either
Mocks[http://github.com/danielsdeleo/moqueue]
or {synchronous libraries}[http://github.com/celldee/bunny]
instead of EM-based libraries in unit tests. This is not always an option, however -
sometimes your code just has to run inside the event loop, and you want to test a real
thing, not just mocks.

EM-Spec[http://github.com/tmm1/em-spec] gem made it easier to write evented specs, but it
has several drawbacks. First, it is not easy to manage both EM.run and AMQP.start loops
at the same time. Second, AMQP is not properly stopped and deactivated upon exceptions and
timeouts, resulting in state leak between examples and multiple mystereous failures.

AMQP-Spec is based on EM-Spec code but extends it to support AMQP event loops (as well as
RSpec2). API is very similar to EM-Spec's, only a bit extended. The final goal is to
make writing AMQP specs reasonably pleasant experience and dispel the notion that evented
libs are impossible to unit-test.

Mind you, you still have to properly manage your AMQP broker in order to prevent broker
state from leaking between examples. You can try to combine AMQP-Spec and
Moqueue[http://github.com/danielsdeleo/moqueue] if you want to abstract away actual broker
interactions, but still specify some event-based expectations.

== Rspec

There are several ways to use amqp-spec. To use it as a helper, include AMQP::SpecHelper
in your describe block. You then use either #amqp or #em methods to wrap your evented
test code. Inside the amqp/em block, you must call #done after your expectations. Everything
works normally otherwise. You can set default_timeout and default_options to avoid manually
setting AMQP options for each example. However, if you DO supply options to #amqp method
inside the example, they override the defaults.

Default options and default timeout are local for each example group and inherited by
its nested groups, unconnected example groups DO NOT share defaults. Please note that
this is different from EM-Spec where default_timeout is effectively a global setting.

In order to setup/teardown EM state before/after your examples, you'll need to use
*em_before* and *em_after* hooks. These hooks are similar to standard RSpec's
*before*/*after* hooks but run inside the EM event loop before/after your example block.
If you are using #amqp method, *em_before* hook will run just BEFORE AMQP connection is
attempted, and *em_after* is run after AMQP is stopped.

Sometimes, you may want to setup/teardown state inside AMQP connection (inside block given
to AMQP.start): for example, to make sure that the connection is established before your
examples run, or to pre-declare some queues and exchanges common for all examples.
In this case, please use *amqp_before* and *amqp_after* hooks. These hooks run inside
the AMQP.start block just before/after your example block.

  require "amqp-spec/rspec"

  describe AMQP do
    include AMQP::SpecHelper
  
    # Can be used to set default options for your amqp{} event loops
    default_options {:host => 'my.amqp.broker.org', :port => '21118'}

    # Can be used to set default :spec_timeout for your evented specs
    default_timeout 1

    # Runs inside the EM event loop before each block given to #amqp or #em
    em_before { @start = Time.now }

    # Runs inside the AMQP.start before each block given to #amqp
    amqp_before do
      @channel = AMQP::Channel.new
      @queue = @channel.queue('my_queue')
    end

    it "works normally when not using #amqp or #em" do
      1.should == 1
    end
  
    it "makes testing evented code easy with #em" do
      em do
        EM.add_timer(0.5){
          (Time.now - @start).should be_close( 0.5, 0.1 )
          done
        }
      end
    end

    it "runs AMQP.start loop with options given to #amqp" do
      amqp(:host => 'my.amqp.broker.org', :port => '21118')do
        AMQP.conn.should be_connected
        @queue.name.should == 'my_queue'
        done
      end
    end

    it "optionally raises timeout exception if your loop hangs for some reason" do
      proc {
        amqp(:spec_timeout => 0.5) do
        # no done!
        end
      }.should raise_error SpecTimeoutExceededError
    end

  end

Another option is to include AMQP::Spec in your describe block. This will patch Rspec so
that all of your examples run inside an amqp block automatically. A word of caution about
*before*/*after* hooks in your example groups including AMQP::Spec. Each of these hooks
will run in its separate EM loop that you'll need to shut down either manually (#done) or
via timeout. Essentially, this means that any EM-related state that you'd like to set up or
tear down using these hooks will be lost as example itself will run in a different EM loop.

In short, you should avoid using *before*/*after* if you include AMQP::Spec - instead, use
*em_before*/*em_after* or *amqp_before*/*amqp_after* hooks that run inside the EM event
loop. You don't need to call #done inside these evented hooks, otherwise you'll shut down
the EM reactor before your example runs.

  require 'amqp-spec'

  describe AMQP do
    include AMQP::Spec

    default_options {:host => 'my.amqp.broker.org', :port => '21118'}
    default_timeout 1

    em_before { @start = Time.now }

    it "requires a call to #done in every example" do
      1.should == 1
      done
    end
    
    it "runs test code in an amqp block automatically" do

      EM.add_timer(0.5){
        (Time.now - @start).should be_close( 0.5, 0.1 )
        done
      }
    end

    it "runs AMQP.start loop with default_options" do
      AMQP.conn.should be_connected
      done
    end

    it "raises timeout exception ONLY if default_timeout was set" do
      proc do
        # no done!
        end.should raise_error SpecTimeoutExceededError
    end
  end

Finally, you can include AMQP::EMSpec in your describe block. This will run all the group
examples inside em block instead of amqp. Non-evented *before*/*after* hooks should be
finished with #done, same as when including AMQP::Spec, and same caution about using them
applies - use *em_before*/*em_after* instead.

  describe AMQP do
    include AMQP::EMSpec

    it "requires a call to #done in every example" do
      1.should == 1
      done
    end

    it "runs test code in an em block, instead of amqp block" do
      start = Time.now

      AMQP.conn.should be_nil

      EM.add_timer(0.5){
        (Time.now-start).should be_close( 0.5, 0.1 )
        done
      }
    end
  end

== General notes

For a developer new to evented specs, it is not easy to internalize that the blocks given
to asynchronous methods are turned into real callbacks, intended to fire some time later.
It is not easy to keep track of the actual execution path of your code, when your blocks
are supposed to fire and in what sequence.

Take the following spec as an example:

  it 'receives published message' do
    amqp do
      q = MQ.queue('queue_name')
      q.subscribe do |hdr, msg|
        msg.should_not == 'data'
      end
      MQ.queue('queue_name').publish 'data'
      q.unsubscribe
      q.delete
      done
    end
  end

Seems like a straightforward spec: you subscribe to a message queue, you set expectations
inside your subscribe block, then you publish into this queue, then you call done. What may
be wrong with it? Well, if you happen to use this spec against live AMQP broker, everything
may be wrong. First, communication delays. There is no guarantee that by the time you
publish your message, the queue have been either created or subscribed to. There is also
no guarantee that your subscriber received the message by the time you are unsubscribing
and deleting your queue. Second, sequence of your blocks. Remember they are delayed
callbacks! Don't just assume your previous block is already executed when you start your
new asynchronous action. In this spec, when done is called, it stops everything before your
subscribe callback even has a chance to fire. As a result, you'll get a PASSING spec even
though your expectation was never executed!

How to improve this spec? Allow some time for async actions to finish: either use EM timers
or pass :nowait=>false to your asynch calls to force them into synchronicity. Keep in mind
the sequence in which your callbacks are expected to fire - so place your done call at the
end of subscribe block in this example. If you want to be paranoid, you can set flags inside
your callbacks and then check that they actually fired at all AFTER your amqp/em block.
Something like this will do the trick:

  it 'receives published message' do
    amqp do
      q = MQ.queue('queue_name')
      q.subscribe do |hdr, msg|
        @subscribe_fired == true
        msg.should == 'data'
        done {q.unsubscribe; q.delete}
      end
      EM.add_timer(0.2) do
        MQ.queue('queue_name').publish 'data'
      end
    end
    @subscribe_fired.should be_true
  end

== Legacy EM-Spec based specs support

In order to run your existing EM-Spec based specs unmodified, instead of require 'em-spec':

  require 'amqp-spec'
  require 'amqp-spec/em_spec_shim'

Please note that in amqp-spec default options and default timeout are local for each
example group and inherited by its nested groups, unconnected example groups DO NOT
share defaults. This is different from EM-Spec where default_timeout is effectively
a global setting. So please make sure you're setting default_timeout early on,
in your outermost example group in order to get same behavior as in EM-Spec.

== Extending library to spec other types of event loops

AMQP-Spec extends EM-Spec code to cover different types of event loops.
Initially, vanilla EM and AMQP event loops are supported, but the lib can be extended
quite easily to spec any other type of event loop, not necesarily EM-based (such as
Cool.io, ZMQMachine and whatnot). All you need to do is just write appropriate
EventedExample subclass (with minimal interface of just #run and #done to start and
stop your loop) and RSpec wrapper for it similar to SpecHelper#em or SpecHelper#amqp.
You can then call your wrapper in RSpec examples in a way similar to #em or #amqp.

== Limitations

AMQP-Spec can be currently used with Rspec only. I suppose, it is not that difficult to
extend EM-Spec's Test::Unit and Bacon support, I just do not have experience doing it.

Any help improving this library is greatly appreciated...

== LICENSE:
Copyright (c) 2010-2011 Arvicco.
Original EM-Spec code copyright (c) 2008 Aman Gupta (tmm1)

See LICENSE for details.