Chuck Remes
8/12/2008 11:00:00 AM
On Aug 11, 2008, at 10:11 PM, James Gray wrote:
> On Aug 11, 2008, at 9:37 PM, Chuck Remes 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?
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