Sometimes it’s fun to fall down a coding rabbit hole — means you’re learning something. Tonight I began seriously messing with acts_as_state_machine as part of a skunkworks project I’m hoping to roll to alpha in the next couple weeks, and I ran into an interesting pickle when attempting to write basic RSpec to test valid state change: either the AASM isn’t persisting to the db, or my understanding of the RSpec model lifecycle is not what I had thought. That’s the limited amount I’ve been able to glean since hitting the wall, and what I’m going to dive back into after taking a break writing this post.
AASM works by establishing some states, which are tied to instance methods, which support callbacks. So you can declare any given model to act as a finite state machine. Real-world examples of this include users of a web service (who might be “inactive”, “active”, “suspended”, “pending”, etc), and orders in an e-commerce system, (which might be “new”, “pending payment”, “shipped”, etc).
Any system with a finite number of states can be modeled, in theory, as a finite state machine. With Rails AASM, is the easiest way to do this — going by the theory that you should always download whatever you can.
Here are the events and the initial states for my object:
1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt><strong>5</strong><tt>
</tt>6<tt>
</tt>7<tt>
</tt>8<tt>
</tt>9<tt>
</tt><strong>10</strong><tt>
</tt>11<tt>
</tt>12<tt>
</tt>13<tt>
</tt>14<tt>
</tt><strong>15</strong><tt>
</tt>16<tt>
</tt>17<tt>
</tt>18<tt>
</tt>
|
acts_as_state_machine <span class="sy">:initial</span> => <span class="sy">:active</span><tt>
</tt><tt>
</tt> state <span class="sy">:active</span><tt>
</tt> state <span class="sy">:dormant</span><tt>
</tt> state <span class="sy">:dropped</span>, <span class="sy">:enter</span> => <span class="sy">:do_drop</span> <tt>
</tt> <tt>
</tt> <tt>
</tt> event <span class="sy">:activate</span> <span class="r">do</span><tt>
</tt> transitions <span class="sy">:from</span> => [<span class="sy">:dropped</span>, <span class="sy">:dormant</span>], <span class="sy">:to</span> => <span class="sy">:active</span> <tt>
</tt> <span class="r">end</span><tt>
</tt> <tt>
</tt> event <span class="sy">:dormantize</span> <span class="r">do</span><tt>
</tt> transitions <span class="sy">:from</span> => <span class="sy">:active</span>, <span class="sy">:to</span> => <span class="sy">:dormant</span><tt>
</tt> <span class="r">end</span> <tt>
</tt> <tt>
</tt> event <span class="sy">:drop</span> <span class="r">do</span><tt>
</tt> transitions <span class="sy">:from</span> => [<span class="sy">:active</span>, <span class="sy">:dormant</span>], <span class="sy">:to</span> => <span class="sy">:dropped</span><tt>
</tt> <span class="r">end</span> <tt>
</tt>
|
The rabbit hole I fell down is trying to figure out why the state change is so hard to validate with RSpec. This test passes:
1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>
|
it <span class="s"><span class="dl">"</span><span class="k">should default to state of 'active'</span><span class="dl">"</span></span> <span class="r">do</span><tt>
</tt> <span class="iv">@user_feed</span>.state.should eql(<span class="s"><span class="dl">"</span><span class="k">active</span><span class="dl">"</span></span>)<tt>
</tt> <span class="r">end</span><tt>
</tt>
|
As well it should, since @user_feed is made by an RSpec helper method that actually creates an object of class ActiveRecord::Base::UserFeed in the database, and I added a default value to the db schema. The AASM stuff works on the console with no problems, and it responds to the AASM-created instance methods in RSpec tests. But the state change itself isn’t testing correctly:
This works:
1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>
|
it <span class="s"><span class="dl">"</span><span class="k">should respond to 'drop!</span><span class="dl">"</span></span> <span class="r">do</span><tt>
</tt> <span class="iv">@user_feed</span>.respond_to?(<span class="s"><span class="dl">"</span><span class="k">drop!</span><span class="dl">"</span></span>).should eql(<span class="pc">true</span>)<tt>
</tt> <span class="r">end</span><tt>
</tt>
|
This doesn’t:
1<tt>
</tt>2<tt>
</tt>3<tt>
</tt>4<tt>
</tt>
|
it <span class="s"><span class="dl">"</span><span class="k">should change state to 'dropped' with #drop!</span><span class="dl">"</span></span> <span class="r">do</span><tt>
</tt> <span class="iv">@user_feed</span>.drop!<tt>
</tt> <span class="iv">@user_feed</span>.state.should eql(<span class="s"><span class="dl">"</span><span class="k">dropped</span><span class="dl">"</span></span>)<tt>
</tt> <span class="r">end</span><tt>
</tt>
|
Now I still consider myself an RSpec n00b, and in a lot of ways, a Rails n00b as well, but it seems to me that I should basically be able to replicate the same ActiveRecord lifecycle in RSpec that I can in the console. In fact, this is the first time I can remember w/ using RSpec that I can’t.
A clue to the post-predicament steps comes from this blog post, in which the author has written a custom matcher for the AASM transition. That was going to be my next step after writing a basic test that simply ensures the state is changing correctly in the DB column. But first, I want to understand what’s going on with the inability to duplicate my console success in RSpec. I’m sure it’s going to end up being something tiny or obvious or both — ever thus with programming.
I await the next iteration of my ongoing humbling.