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.”
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.
Make reuse and testing easier by splitting a problem into smaller parts.
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
- 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.
- Test and create a new
scopewith an intention revealing name in the ActiveRecord model that encapsulates the functionality of the original
wherestatement, and accepts any necessary parameters.
- Replace the original
- 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
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.
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:
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.
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
scopeliving in the appropriate class, it will be easier to find when developers are looking for it, which makes it easier to reuse and maintain.