Extract Scope: where should the code live?
In Rails, accessing specific records from the database can at times become unnecessarily complex.
The complexity is compounded when this code is jammed in the middle of an already bloated controller action. Naturally this has a knock on effect on the complexity of testing the new functionality.
In these situations, scope is an invaluable tool.
From the Rails docs, scope
: “Adds a class method for retrieving and querying objects. The method is intended to return an ActiveRecord::Relation object, which is composable with other scopes.”
A scope
has a single responsibility to retrieve data, and on its own is straightforward to test.
By using Extract Scope, you can divide and conquer the current issue of testing the bloated controller action into two parts, each being easier to manage and enabling code reuse and ease of maintenance and testing.
Motivation
Make reuse and testing easier by splitting a problem into smaller parts.
Code Smells
-
Tests are painful to write due to the current home of the source code.
-
where being called outside of an ActiveRecord::Model.
In the following example, the admin area of a site has a page that lists the Orders that are ending today, allowing the sales department to reach out to customers whose account is about to lapse.
Due to the volume of code surrounding the where
call:
- Tests get missed.
- Tests become hard to write, which discourages developers from writing tests for this code.
- Code isn’t easily reusable.
- Code isn’t easily found, which results in similar code being duplicated throughout the codebase.
Mechanics
The mechanics of Extract Scope are a Rails specific implementation of Extract Method/Function from “Refactoring” by Martin Fowler.
- Test and create a new
scope
with an intention revealing name in the ActiveRecord model that encapsulates the functionality of the originalwhere
statement, and accepts any necessary parameters. - Replace the original
where
call withscope
. - Potentially, simplify original tests to check interaction (spy/mock) with new, fully tested
scope
, rather than have unnecessary database interaction.
Step 1: Test and Introduce new scope in Order model
Step 2: Replace the original where call with scope
Having put the new scope
in place, now it is time to start using it in the controller.
At this point the original controller tests should still be passing.
Step 3: Simplify original tests
Having documented the public interface of Order.ending_on
with the model specs, a response can reliably be faked (stubbed in this case) in the controller specs, which in turn check the interaction between Controller and Model.
All good unit tests should avoid connecting to the database, however with Rails this is a line that is always crossed. This doesn’t mean there shouldn’t be an effort to minimise this.
In terms of keeping our test suite running fast, it makes sense to invest in this clean up by stubbing Order.ending_on
and returning a non-persisted instance of Order
.
Things to Consider
1: Clarity of the test
The original controller test was very clear to understand, even if it did not cover all the necessary cases.
In the last code example the stubbing reduces that clarity a little, but not to a significant extent.
Only you can be the best judge of which approach to take in your context.
2: Different approaches
Rather than introducing the Order.ending_on(end_date)
method with the end_date
parameter, another approach could have avoided the parameter by encapsulating the knowledge of the end_date within the method.
Below the scope
is now Order.ending_today
, without any parameters. However it is not as flexible, or as easy to test as it can be.
3: Complexity and our example
The example used was fairly simple to aid understanding:
When a where
calls parameters grow in complexity, the number of tests required grow quickly too.
The context within your codebase matters. Treat anywhere you see where
as flag to consider Extracting Scope, and decide if this is the time to do it.
Summary
By “Extracting Scope”, we take a divide and conquer approach when seeing where
being called outside of an ActiveRecord model. This allows us to:
-
See the wood from the trees. Moving the code into the model lets us see and test all the necessary cases without the additional baggage of what is happening in the controller action.
-
Encapsulates knowledge within the
Order
class. -
With the
scope
living in the appropriate class, it will be easier to find when developers are looking for it, which makes it easier to reuse and maintain.