gocardless/statesman

Documentation - How to test Statesman implementations

pacso opened this issue ยท 11 comments

Hello,

Considering this is quite a new project, it's no surprise that the documentation is a little thin. There's enough to get you going with a working state machine, however there's no mention of the best way to test states/transitions/guards/etc from within an application that uses statesman.

I'm more than happy to help contribute towards the documentation, but just wondered if you have any details on how you currently test models which have a statesman state machine behind them? Do you have any RSpec custom matchers? Or do you not test the implementation of your state machines in a unit-test style but rather in functional tests?

Hey @pacso,

We do test the implementation of our models that use our state machines, just with regular Rspec matchers, not with any custom ones. We definitely should have a sample test in our README though, will try to get that added.

On a rough level, our specs tend to look like:

describe SomeModel do
  let(:some_model) { FactoryGirl.build(:some_model) }

  it "is in state foo by default" do
    expect(some_model.current_state).to eq("foo")
  end

  describe "guards" do
    it "cannot transition from state foo to state bar" do
      expect { some_model.transition_to!(:bar) }.to raise_error(Statesman::GuardFailedError)
    end

    it "can transition from state foo to state baz" do
      expect { some_model.transition_to!(:baz).to_not raise_error
    end
  end
end

So we test our guards pretty heavily by just making sure that transition_to! does or does not raise an error.

Does that help a little?

Jack

Hi Jack,

Thanks for the reply - that's quite useful. The only thing I need now is a method for creating a model in a particular state. For example, imagine a complex state machine where there are many branching paths and guards throughout to control the transitions ... how would you instantiate an object somewhere in the middle of that without manually generating the Transition record for the previous state?

It's not something we've had to do, so there isn't really great support for it right now.

The two options for this are to do what you suggest and manually generate the transition, or you could define a method to step through the states.

Neither is especially neat unfortunately, I suppose it depends on your data which you'd rather do.

I've come up with a reasonably neat solution. I'd be interested to hear your thoughts.

I have an OrderItem model which has a state machine to manage the process of handling an order.
In order to test the transition from any given state to another one, you need to create an OrderItem instance in that given state.

Using FactoryGirl I now have factories along the following lines:

FactoryGirl.define do
  factory :order_item_transition do
    to_state :unassigned
    sort_key 0
    order_item
  end

  factory :order_item do
    ignore do
      current_state :unordered
    end

    after(:create) do |order_item, evaluator|
      if evaluator.current_state != :unordered
        create :order_item_transition, to_state: evaluator.current_state, order_item: order_item
      end
    end
  end
end

This allows me to create an OrderItem within a spec with any required state from my state machine:

RSpec.describe OrderItem, :type => :model do
  describe 'transitions' do
    it 'is in state unordered by default' do
      expect(build(:order_item).current_state).to eq 'unordered'
    end

    it 'transitions from unassigned to order_required upon assignment event' do
      order_item = create(:order_item, current_state: :unassigned)
      expect { order_item.trigger!(:assign_to_orderer) }.to change { order_item.current_state }.from('unassigned').to('order_required')
    end
  end
end

Doesn't seem too messy. Maybe others would find this useful.

@pacso we can't really do that because we have such tight guards on our state machines, due to the nature of what we're modelling. If we wanted to transition through we'd have to go one at a time.

Your solution seems neat could you also post up the source for the state machine? I think this would make a nice addition to the README ๐Ÿ‘

@pacso We mock state machine statuses like this (for Order with state machine transitions persisted as OrderTransitions):

factory :order do
  property "value"
  ...

  trait :shipped do
    after(:create) do |order|
      FactoryGirl.create(:order_transition, :shipped, order: order)
    end
  end
end

factory :order_transition do
  order
  ...

  trait :shipped do
    to_state "shipped"
  end
end

This gets around the transtition guards since there are no guards on creating a transition record, only performing the transition. You could then do:

let(:shipped_order) { FactoryGirl.create(:order, :shipped) }
it "transitions to returned" do
  expect { shipped_order.transition_to!(:returned) }.to_not raise_exception
end

We have now added some of this to the README. @pacso if there's anything we missed, please comment and let us know, and thanks for raising this issue ๐Ÿ‘

Hey @PasCo @jackfranklin would you guys mind posting the contents of one of your SomeModelTransition factory(-ies)? We don't use Factory Girl, so I am curious to see how you guys populate the null: false columns on your OrderTransition model, for example sort_key and most_recent. Thanks :)

pacso commented

Hi @mecampbellsoup - the FactoryGirl solution I mentioned above is actually what I'm still using. It provides enough to be able to unit test my state machine. For integration tests I don't use any factories for orders or transitions.

Oh okay. So the factory must be filling in defaults for those null: false
attributes. What does ModelTransitionFactory.build return for you?

On Thu, Jan 21, 2016, 5:41 PM Jon Pascoe notifications@github.com wrote:

Hi @mecampbellsoup https://github.com/mecampbellsoup - the FactoryGirl
solution I mentioned above is actually what I'm still using. It provides
enough to be able to unit test my state machine. For integration tests I
don't use any factories for orders or transitions.

โ€”
Reply to this email directly or view it on GitHub
#77 (comment)
.

@mecampbellsoup You can just make sort_key: 0 and most_recent: true as it would be the first (and presumably only) transition at the time.

I find this to be a fairly decent solution to bypassing guards and just putting the model into some state in order to be able to test business logic. Thanks @pacso!