I’m back programming on Rails after a few months away. Apart having to get reacquainted with most of the tools, it took me a little while to get back on my feet regarding my workflow.
One difficulty was getting to do unit tests the right way. After struggling a bit with rspec matchers, I faced two different issues: how to test a significantly procedural algorithm (which, believe me, was already as much OO as possible), and the second one, which I’ll talk about in this post, the testing of ActiveRecord callbacks.
Regarding a specific model, I needed to test a whole lot of business logic implemented in its before_save callback chain (let’s not dive into that, please). This callback chain consisted of 5 different methods, each with one specific responsibility. The correct implementation of the business logic depended on the callback order and, of course, on the correctness of each one of them.
I started by testing each single callback, by performing always the same strategy: setting up initial model/fixtures to ensure a specific test case was being stimulated, running model.save! and either checking state or message expectations afterwards. This worked pretty well, except for one thing: when calling save we’re actually involving a whole lot of logic, not only of the callback we’re currently interested in, but also additional callbacks and other ActiveRecord interactions.
The result was that to test a single aspect involved in the whole callback chain, my tests had to setup fixtures or mock other unrelated aspects. Additionally, calling save triggers database interaction which tends to lead to pretty slow test suites. I remember in the past having to deal with extremely long-running test suites just for the excessive use of persistence operations, which aren’t really necessary most of the time. It didn’t take much time for me to realize I was doing it all wrong again, like in the good old days. The final approach I took was the following:
A first few tests were added to, first, ensure the callback order was being respected, which would also serve as documentation to other developers (this is quite relevant, since relying on callback order for correctness tend to be quite risky – but, well, let’s not discuss that either now). Secondly, assert overall business logic / behavior after the whole save / callback chains would lead to expected results. These initial tests were the only ones touching the database. After these, dozens of other specific methods testing just the specific callbacks being executed at each point.
I like to think of this approach as a divide and conquer strategy. First we ensure the correct messages are being sent (which alerts us in case someone accidentally remove a before_save callback from the chain), and that they go in the correct order. Secondly, we ensure each small message do their own job correctly. I’d say this is all unit testing, but this approach creates a relation between those two types of tests similar to the one between integration and unit tests.
The resulting test suite is pretty comprehensive (assuming one does a good job when testing the isolated callbacks), and at the same time they’re blazing fast.