Refactoring a Concern into a Service Object
I like abstractions, those little buckets that hide complexity and, in turn, make everything easier to reason about. For a time, I really appreciated using
ActiveSupport::Concern to hide complexity and increase re-usability in my Rails controllers. But lately, I’ve switched to plain old ruby objects.
Why? POROs are easier to test.
The Problem with Controller Concerns
Let’s say we have a Controller and it’s Concern:
This works, right? But let’s consider our options for testing. . .
Option 1: Check for the Side Effect in the Controller Test
This is sub-optimal. While there is a place for this high-level integration test, we have no confidence that the
ExpertMixer was the thing that mutated this object’s state. It is conceivable that something else could have toggled the
You also have to build the whole world around the concern just so you can test it (params, dependent objects, etc.). No thanks.
assigns are naughty, and I believe are going away in Rails 5.
Option 2: Isolate out a Dummy Controller
This approach successfully isolates the concern, but at what cost? If you absolutely require an
ApplicationController and all of its bits, then it may be useful, but I try not to write tests that test the framework I’m using.
Wouldn’t it be better to just test the mutator in isolation?
Option 3: Refactor to a Service Object
Let’s drop down a level, extract an object to handle this interaction, and then see how much it helps the testing scenario.
ActiveSupport::Concern, just a plain old ruby object that is trivial to test in isolation. We also get the added documentation on where the
stir_it method is coming from, which becomes relevant when you have several concerns modifying the same controller.
You may argue, “Hey Zack! This works because you really aren’t doing a Controllery thing here, you are modifying a model.”
Well, that’s true, but we are talking about Object Oriented Programming. Almost anything can be objectified and extracted in this manner, even Controllery things.
The test is as easy as we’d expect:
We know that the
ExpertMixer mutates the
mixable object exactly as we expect. We can then add an integration test for more confidence, if we want.
An added bonus of this approach is that it keeps the surface area of the controller under control. All those methods that would have been decorating it are tucked away in the PORO.
Of course, there’s still plenty of places for
ActiveSupport::Concern, but I find I’m reaching for it considerably less often than I used to.