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

== Summary

Simple API for writing asynchronous EventMachine/AMQP specs. Supports RSpec and RSpec2.

== 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
is supposed to run inside event loop, and you want to test a real thing, not 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 AMQP library state leak
between examples and multiple mystereous failures.

AMQP-Spec is built upon EM-Spec code but makes it easier to test AMQP event loops specifically. API is
very similar to EM-Spec, only a bit extended. The final goal is to make writing AMQP specs reasonably
pleasant experience and dispel the notion that evented AMQP-based 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 use default_timeout and default_options
macros to avoid manually setting AMQP options in each example. However, if you DO manually set options inside
the example, they override the defaults.

Default options and default timeout are local for each example group and inherited by its nested groups,
different example groups can have separate defaults. Please note that this is different from em-spec where
default_timeout is effectively a global setting.

  require "amqp-spec/rspec"

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

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

    before(:each) do
      puts EM.reactor_running?
    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
        start = Time.now

        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
        done
      end
    end

    it "optionally raises timeout exception if your loop hangs for some reason" do
      proc {
        amqp(:spec_timeout => 3){}
      }.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{} and 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 default_timeout. Essentially, this means that any EM-related state that you'd like
to setup/teardown using these hooks will be lost as each example will run in a separate EM loop. In order to
run setup/teardown hooks inside the EM loop, you'll need to use before_amqp{} and after_amqp{} hooks that run
inside the EM loop but before/after AMQP loop (these hooks are currently not implemented)

  describe AMQP do
    include AMQP::Spec

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

    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
      start = Time.now

      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{}.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. before{} and after{} hooks should be finished with 'done', same as
when including AMQP::Spec, and same caution about using them applies.

  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


==Bacon

...

==Test::Unit

...

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

==Limitations

AMQP-Spec can be currently used with Rspec only. I suppose, there is nothing special in extending EM-Spec's
test unit and bacon support, I just do not have experience dealing with these platforms. Another limitation,
it uses native Fibers and therefore not compatible with Ruby 1.8. Again, it seems possible to rewrite it in
1.8-compatible style, with string evals and Fiber backport, but I'd rather leave this work to someone else.

Any help improving this library is greatly appreciated...

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

See LICENSE for details.