Statemachine gem and unit testing

I’ve been using the statemachine gem [1] to build out some finite
state machines for some work I am doing. I generally use rspec for
most of my spec driven development, but I confess to having a bit of
trouble wrapping my head around how to write specs for these
statemachines.

For very simple 1 to 3 state machines, spec’ing the behavior is
relatively simple. However, when the machines start getting more
complex with nested superstates and more than a few transitions,
spec’ing the behavior quickly gets out of control. Throw in a few
decision states that can branch 2+ ways and my head is fit to explode.

I’ve been writing the specs iteratively. Each time I add a state, the
#before block gains a few more lines to “drive” the machine to that
specific state so I can test its behavior. Obviously this block gets
pretty unwieldy after just a few semi-complex states. The specs that
ship with the gem are not much help in this regard; they are very
simple.

The obvious answer is to decompose every machine so none are larger
than 3 states (or so), but then it pushes the complexity out to
handling the interaction between those machines. Another approach
would be to break encapsulation and allow the specs to setup the state
directly without “driving” the machine to populate the values, but
that just feels wrong.

Any ideas? Example code would be appreciated! :slight_smile:

cr

On Aug 11, 2008, at 9:37 PM, Chuck R. wrote:

I’ve been using the statemachine gem [1] to build out some finite
state machines for some work I am doing.

This is an absolutely excellent point you have raised about a very
real problem. I totally confess that I’m just throwing out an idea
here, so feel free to ignore this if it’s unhelpful.

You did say the statemachine is for some work. I’m wondering if it
would be possible to test that outer layer, the real work. The
statemachine is just an implementation detail anyway, right? I mean,
maybe it would be hard, but you could probably replace it with a
different strategy. As long as the work still got done, of course.
Can you test that?

Good luck with your problem?

James Edward G. II

On Aug 11, 2008, at 10:11 PM, James G. wrote:

would be possible to test that outer layer, the real work. The
statemachine is just an implementation detail anyway, right? I
mean, maybe it would be hard, but you could probably replace it with
a different strategy. As long as the work still got done, of
course. Can you test that?

This is what I am doing at the moment. I am sending the object events
and testing for the side effects. Nearly all of the work it is
performing is visible as a side effect on a mock. For example, if I
send it a parameter_update_event it moves to the ParameteChange
decision state and then the ModeChange decision state before it lands
in the correct “mode” state. I exposed just enough of the internals
soI can assert that object.fsm.state.should == :correct_state.

Another more complex side effect is when I sent it a
price_update_event. It moves from Standby to PriceCalculation decision
state. If the update presents a good “opportunity,” the machine moves
to a state where it steps through all of the logic for spawning off
other objects to handle the opportunity, otherwise it goes to rest in
the Standby state awaiting a new event.

Last night I broke one of my machines up into smaller bites. It
appears as though each machine gets pretty tough to test beyond 4
states. Here’s an example of what I mean. This before block has to
setup a bunch of mocks so each side effect guides the machine through
the steps that I need it to go.

describe BidQuoter, “superstate :continuous, :standby_continuous
substate” do
before(:each) do
@q = BidQuoter.new
@worker = mock(“quoter worker”, :null_object => true)
@bar = mock(“bar”)
@edge = mock(“edge”)
@order = mock(“order”)
@parameter = mock(“parameter”)
@trade = mock(“trade”)

@bar
.should_receive(:event_type).any_number_of_times.and_return(:bar_update)

@edge
.should_receive
(:event_type).any_number_of_times.and_return(:parameter_update)

@order
.should_receive
(:event_type).any_number_of_times.and_return(:order_update)

@parameter
.should_receive
(:event_type).any_number_of_times.and_return(:parameter_update)

@trade
.should_receive
(:event_type).any_number_of_times.and_return(:trade_update)

 @q.worker = @worker # a sub fsm

@parameter
.should_receive(:key).any_number_of_times.and_return(:quote_mode)

@parameter
.should_receive(:quote_mode).any_number_of_times.and_return(‘c’)
@edge.should_receive(:key).any_number_of_times.and_return(:edge)
@edge.should_receive(:edge).any_number_of_times.and_return(-0.02)
@q.run(1)

 # send these two events to drive the machine to the correct state
 @q.update(@parameter)
 @q.update(@edge)

end

2 or 3 “it should” blocks

end

All that setup is required to get the machine into a state where I can
then send more specific events and verify it is creating the right
side effects and transitioning to the right states. Several of those
mocks are used in the “it should” blocks that come afterwards but I
set them up here because they are used multiple times. This setup
block gets longer the “deeper” I get into the machine and test each
branch with more mocks being created and additional assertions being
assigned to them.

Ugh… state machines make it very simple to layout the logic but
testing it is an absolute mess. All of this “setup” is a classic code
smell but I don’t see any alternative right now. I’m betting that I am
missing a simple technique or heuristic for managing this better.

cr

2008/8/12 Chuck R. [email protected]:

Ugh… state machines make it very simple to layout the logic but testing
it is an absolute mess. All of this “setup” is a classic code smell but I
don’t see any alternative right now. I’m betting that I am missing a simple
technique or heuristic for managing this better.

Chuck, there’s a Google or Yahoo group for test-driven development.
Maybe you can ask there, too. I’m also interested in this topic, so
I’d be glad if you could post some results here.

Regards,
Pit

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs