Managing object interdependence is a challenge when designing object-oriented software. Decoupling objects, separating responsibilities, and establishing boundaries of communication are important tools for reducing dependencies. This post addresses reducing dependencies on one particular type of object: those which inherit from
ActiveRecord, a core Rails component.
ActiveRecord objects possess a powerful and globally available interface for building SQL queries, tempting developers to scatter coupling throughout applications. Moreover, the chainability of this interface can present testing difficulties. To combat these hazards, we will wrest control of our public APIs from
ActiveRecord and write custom test doubles to better represent our
We are writing an application which has Users, and we need to search for a given User by name or email.
The search feature should support partial matches and be case insensitive. For example, “da” should return Users with first name “Dan” as well as Users with the last name “Davidson”. Let’s take a look at our bare bones class structure.
An Aside About Structure
A Controller’s Job
A controller is an interface to our application. It is a mediator between an incoming request and the application core, and it should remain largely ignorant of the application structure.
We have prematurely extracted the search functionality from the
UserSearch class is a mechanism for the controller to ignorantly call into the application; a narrow point of contact on the application’s surface area.
What, Not How
This aspiration of controller simplicity is often summarized
A controller should know what to do, but not how to do it
A controller issues imperatives, but should not know how an operation is carried out. The “how” of an operation should be delegated to the application core so as to insulate the controller from shifts in application infrastructure. In this case, the controller knows it wants to search for
Users, but needn’t care about the logistics of the search.
An Initial Implementation
Our first implementation of the
UserSearch will use the ActiveRecord supplied
where method to directly build a query of the
UserSearch class is composing a query for
Users. The initial implementation of tests will need to ensure the query is correctly constructed by inserting records into the database before each test.
If you’ve worked with Rails, chances are you’ve written or seen code similar to this example. The
ActiveRecord query interface is a powerful tool for building SQL queries, and is available on any class which inherits from
Unfortunately, our use of these powerful tools has come at a cost: we have heavily coupled the
UserSearch to the
User class. Coupling refers to the degree to which one class must know about another class. The
UserSearch class knows many things about the
User class and
- It knows the
userstable has “first_name”, “last_name”, and “email” columns.
- It knows these three
userstable columns contain strings, and we can use the “LIKE” operator.
- It knows that User inherits a
ActiveRecord. Though relatively innocuous, this coupling could cause issues if we need to upgrade or swap out
Additionally, our tests have some issues.
- We cannot run our tests without the presence of a database in which to insert records.
- We must create Users with every combination of attributes in order to test that our scopes are properly combined.
Why is Coupling Bad?
Coupling makes code harder to change, and code will always change. Requirements change, businesses change, application structure evolves; change is the law of development. Effective developers must abide this law and author code which absorbs change quickly and easily.
So, if coupling is bad, and changeability is good, how can we reduce our coupling with the
Clearly, we’re going to have to depend on the
User class. There is no way around it – we want
User instances. However, we can empower the
User class to better control the boundaries of communication.
Objects which inherit from
ActiveRecord have boundary issues.
ActiveRecord imbues inheriting classes with a large public API (set of publicly available methods) that is easily abused.
where method, for example, is available anywhere else in our application. It is tempting to disemminate dependencies on this method throughout our code, as well as dependencies on specific database columns.
We should adopt a convention of treating
ActiveRecord supplied query methods as though they are private, except in extenuating circumstances.
This adoption will force us to focus on creating our own public APIs, and reduce the temptation to scatter references to
User needs a well defined public API. Other classes will only query for
User records through
User‘s public API, and should steer clear of
User‘s internal methods (or internal-ish methods, remembering our convention).
This notion of designating User’s internal methods as off-limits and communicating with User via approved channels is how we establish boundaries. By observing these established boundaries, we can reduce our dependence on
User and make our code more resilient to change.
User‘s public interface:
We can now test
User‘s methods directly, and more rigorously than was reasonable within the UserSearch class.
Moving along, we can also update our
UserSearch tests need to be updated as well.
We have reduced our dependency on
User to a few small, explicitly defined methods, but our tests are still suffering.
- The “combines options” example reads poorly. The
user_scopedouble has to return the
scoped_by_namedouble, which has to return the
scoped_by_name_and_emaildouble; doubles returning doubles returning doubles. The spec is long and unnaturally structured, and demands inordinate brain cycles from the reader.
- We’ve introduced a new form of coupling: implementation coupling.
ActiveRecordquery building methods such as
orderare chainable and order insensitive. For example,
User.name_matching(name).email_matching(email)is equivalent to
User.email_matching(email).name_matching(name). Our tests do not allow for such reordering because they enforce one particular sequence of methods. We have placed unnecessary restrictions on the implementation of the
resultsmethod, and we have sacrificed changeability.
This is typically the point where a developer might say “Yes, this test is isolated, but it is painful to write, confusing, and doesn’t provide value. I’ll just integrate with
User because at least that proves something.” With the tests in this state, I would be sympathetic to such an argument.
Transcending the Double
The central problem with our tests is that
ActiveRecord scopes (instances of
ActiveRecord::Relation) are chainable and Rspec doubles are not chainable. We are attempting to use a double to stand in for a dependency which is mechanically different from a double. Let’s introduce a vanilla Ruby object to serve as a test double.
When we invoke
email_matching on an instance of
MockActiveRelation, we receive a new
MockActiveRelation instance, but initialized with a record of our method invocation. Making use of Rspec’s predicate matchers, our updated tests look like this:
MockActiveRelation is a better stand in for
User‘s chainable interface. Through Rspec predicate matchers, we have gained natural readability, and our implementation is decoupled from our tests.
Reduce and Reuse
Perhaps we can abstract this approach and create a general purpose stand in for
MockActiveRelation must be initialized with the methods they support.
relation = MockActiveRelation.new(:name_matching, :email_matching)
We can check if the relation was scoped by calling
relation.scoped_by?(:name_matching, "Lee") # => false relation.name_matching("Kara") relation.scoped_by?(:name_matching, "Lee") # => false relation.name_matching("Lee") relation.scoped_by?(:name_matching, "Lee") # => true
Let’s rewrite our
Doubles are used to stand in for real collaborators in our system. They allow us to express how we depend on a collaborator without truly exercising the collaborator. This object independence allows us to test one object at a time, holding the rest of the world constant.
A useful double presents the same interface as the collaborator it represents. Rspec (or minitest) doubles are great for reproducing simple interfaces, and hopefully most interfaces are simple. Occasionally, though, we run across interfaces which are richer than a trivial message-response.
ActiveRecord::ActiveRelation makes use of method chaining, and is an example of a rich interface.
It is imperative for a double to represent an interface faithfully. Sometimes, this means departing from standard test doubles and authoring custom double objects. Custom double objects enhance the readability of specs, but more importantly, better absorb changes within the implementation.