RSpec'ing acts_as_state_machine

One of my favorite plugins I've seen so far is acts_as_state_machine. It's a dead simple way to model the different states your models can be in. It also lets you register callbacks to when a model enters, entered, or leaves a particular state. It's absolutely fantasic until I have to test it. Then it becomes an absolute nightmare.

The first intuitive, but horrifically wrong idea is to stub out the current state:

@model.stub!(:state).and_return('old_state')
@model.some_event!
@model.state.should == 'new_state'

The problem with this is the mock will always return old_state, even if some_event! caused @model to go into new_state.

A less intuitive, but workable solution is to check that the transition event was fired:

@model.should_receive(:update_attribute).with(@model.class.state_column, "matched")

This is a little nicer, but kind of obscures the intention of the test. So ideally, I'd like to be able to say something like:

@model.should transition_to('matched').from('draft')

Thankfully, the crappy RSpec documentation does cover this case. It was easy to write a custom expection matcher:

module ActsAsStateMachineMatchers
  class Transition
    def initialize(expected)
      @expected = expected
    end

    def matches?(target)
      @target = target
      @target.should_receive(:update_attribute).
        with(@target.class.state_column, @expected)
    end

    def failure_message
      <<-MSG
      expected #{@target.inspect} to transition to state
      #{@expected}, but in state {@target.state}
      MSG
    end

    def negative_failure_message
      <<-MSG
      expected #{@target.inspect} to transition to state
      #{@expected}, but in state {@target.state}
      MSG
    end
  end

  def transition_to_state(expected)
    Transition.new(expected)
  end
end

This is one step away from my ideal case because I was too lazy to a Spec::Mocks::Methods with a corresponding Spec::Mocks::MessageExpectation, which is what 'should_receive' and 'with' are. If I ever get unlazy enough to poke into the code more, I could write the analogous 'should_transition_to', and 'from'. This might be a good excuse to open a github account and play with that too :)