From: Chuck Remes Date: 2008-08-12T19:59:49+09:00 Subject: Re: [Q] statemachine gem and unit testing 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