17

Please, help me. I'm confused. I know how to write state-driven behavior of model, but I don't know what should I write in specs...

My model.rb file look

class Ratification < ActiveRecord::Base
  belongs_to :user

  attr_protected :status_events

  state_machine :status, :initial => :boss do
    state :boss
    state :owner
    state :declarant
    state :done

    event :approve do
      transition :boss => :owner, :owner => :done
    end

    event :divert do
      transition [:boss, :owner] => :declarant
    end

    event :repeat do
      transition :declarant => :boss
    end

  end
end

I use state_machine gem.

Please, show me the course.

1
  • Any way to do this without hitting the database? Seems like we should be able to stub out whatever state_machine is using to hit the database and still get all the changes we expect. Commented Jan 11, 2013 at 16:00

4 Answers 4

10

The question is old, but I had the same one. Taking example from state_machine gem :

class Vehicle
  state_machine :state, :initial => :parked do
    event :park do
      transition [:idling, :first_gear] => :parked
    end

    event :ignite do
      transition :stalled => same, :parked => :idling
    end

    event :idle do
      transition :first_gear => :idling
    end

    event :shift_up do
      transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
    end

    event :shift_down do
      transition :third_gear => :second_gear, :second_gear => :first_gear
    end
  end
end

My solution was:

describe Vehicle do

  before :each do
    @vehicle = Factory(:vehicle)
  end

  describe 'states' do
    describe ':parked' do
      it 'should be an initial state' do
        # Check for @vehicle.parked? to be true
        @vehicle.should be_parked
      end

      it 'should change to :idling on :ignite' do
        @vehicle.ignite!
        @vehicle.should be_idling
      end

      ['shift_up!', 'shift_down!'].each do |action|
        it "should raise an error for #{action}" do
          lambda {@job_offer.send(action)}.should raise_error
        end
      end
    end
  end
end

I was using:

  • ruby (1.9.3)
  • rails (3.1.3)
  • rspec (2.8.0.rc1)
  • factory_girl (2.3.2)
  • state_machine (1.1.0)
Sign up to request clarification or add additional context in comments.

Comments

3

The state_machine_rspec gem includes many helper methods for writing concise specs.

 describe Ratification do
   it { should have_states :boss, :declarant, :done, :owner }
   it { should handle_events :approve, when: :boss }
   it { should handle_events :approve, when: :owner }
   it { should handle_events :divert, when: :boss }
   it { should handle_events :divert, when: :owner }
   it { should handle_events :repeat, when: :declarant }
   it { should reject_events :approve, :divert, :repeat, when: :done }
   it { should reject_events :approve, :divert, :repeat, when: :done }
 end

These RSpec matchers will assist with the state_machine specs from a high-level. From here, one needs to write the specs for the business cases for can_approve?, can_divert?, and can_repeat?.

Comments

1

I have written a RSpec custom matcher. It allows to test state flow in elegant and simple way: check it out

Comments

0

Unfortunately, I think you need to put a test for each state -> state transition, which might feel like code duplication.

describe Ratification do
  it "should initialize to :boss" do
    r = Ratification.new
    r.boss?.should == true
  end

  it "should move from :boss to :owner to :done as it's approved" do
    r = Ratification.new
    r.boss?.should == true
    r.approve
    r.owner?.should == true
    r.approve
    r.done?.should == true
  end

  # ...
end

Fortunately, I think this usually fits into integration testing. For instance, an extremely simple state machine for a payments system would be:

class Bill < ActiveRecord::Base
  belongs_to :account

  attr_protected :status_events

  state_machine :status, :initial => :unpaid do
    state :unpaid
    state :paid

    event :mark_as_paid do
      transition :unpaid => :paid
    end
  end
end

You might still have the unit tests as above, but you'll probably also have integration testing, something like:

describe Account do
  it "should mark the most recent bill as paid" do
    @account.recent_bill.unpaid?.should == true
    @account.process_creditcard(@credit_card)
    @account.recent_bill.paid?.should == true
  end
end

That was a lot of handwaiving, but hopefully that makes sense. I'm also not super used to RSpec, so hopefully I didn't make too many mistakes there. If there's a more elegant way to test this, I haven't found it yet.

1 Comment

You can use... @account.recent_bill.should be_unpaid

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.