How to test state machine in Ruby
I could not find any worth solution how to test state machinee with RSpec. Until now I met only one solution which is proposed by stackoverflow habitants. But there are some disadvantages of this approach and in this post I will try to explain what are they and how to avoid them.
What we have
Assume that we have to simulate the following automatic gearbox shifting:
This image will show which operations and states we have:
I use this state machine gem and RSpec for tests. And this is how looks possible class with gearshift levels flow:
Headache
Stackoverflowers proposes to build object and then change state step by step with generated method-events. The possible tests for this situation would look like this:
No so much code, but it has some problems:
- It’s not readable and it’s very easy to miss some typo because of a lot of calls method-events (switch_to_r, switch_to_d, and etc.). It looks like we test not state machine but some field which is changed by some method rather than test events and what they do
- We have to worry about how the state machine works: we set
gear
to appropriate state for any test - If we change state name we will have to fix ALL these tests
- If the
subject
doesn’t have appropriate state a test will be failed
I used to this approach till now but every time I had issues with maintaining tests like above. Eventually I decided to invent some DSL which helps me to get rid of these problems forever. Hopefully I found solution and I’m going to share it with you in this post. Also I would like to know your opinion about it.
Use custom matcher
I hope you know that RSpec has mechanism which allows to create your custom matchers. I’m inspired how the should-matchers is written and how it helps to test my Rails code. So I decided to create some matcher as shoulda-matchers provides:
Let’s see how it can help us to test the given state flow:
Obviously that we have much more code, but it’s readable, maintainable, the tests says about what they do without any description. If we change state machine’s field all what we have to do is to change input data for tests. By the way, we can move our input data (I mean all let’s) to shared example. And then if we had many classes with similar state flow we would use this shared example there.
Conclusion
With this approach I forgot my troubles with testing state machine and created reusable solution which I always will use in my code from now. I have an idea to create gem for this but I’m not sure that it will have any popularity. If you like this solution, please, let me know about it. Thank you for your attention!
I’ve created gist for this solution.
UPDATE: I’ve invented much easier solution. It’s much easier and elegant. Check it out