Isolating and Testing Active Record Dependencies

RSS

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 ActiveRecord dependencies.

Background

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 UsersController into UserSearch. 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 users table.

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 ActiveRecord.

Identifying Coupling

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 users table:

  • It knows the users table has “first_name”, “last_name”, and “email” columns.
  • It knows these three users table columns contain strings, and we can use the “LIKE” operator.
  • It knows that User inherits a where method from ActiveRecord. Though relatively innocuous, this coupling could cause issues if we need to upgrade or swap out ActiveRecord.

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 User class?

Reducing Coupling

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.

Establishing Boundaries

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. User‘s 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 ActiveRecord methods.

Empowering User

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.

Here is 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.

The UserSearch tests need to be updated as well.

Progress?

We have reduced our dependency on User to a few small, explicitly defined methods, but our tests are still suffering.

  1. The “combines options” example reads poorly. The user_scope double has to return the scoped_by_name double, which has to return the scoped_by_name_and_email double; doubles returning doubles returning doubles. The spec is long and unnaturally structured, and demands inordinate brain cycles from the reader.
  2. We’ve introduced a new form of coupling: implementation coupling. ActiveRecord query building methods such as where and order are 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 results method, 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 name_matching or 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 ActiveRecord::Relation instances:

Instances of 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 scoped_by?.

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 UserSearch examples.

Conclusion

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.

Ben Eddy

Ben Eddy Senior Software Developer Ben dreams of being an all-growed-up Ruby programmer. His birth was noble and illustrious, his dignity supported by an adequate patrimony in land and money; and these advantages of fortune are accompanied with liberal arts and decent manners.

Comments (4)

  1. 1
    Reply

    irohiroki

    on February 21, 2014 said:

    Thank you for sharing this, I’m really impressed and love to use this technic in my code. what is the license of MockActiveRelation?

  2. Ben Eddy
    2
    Reply

    Ben Eddy

    on March 5, 2014 said:

    Hi irohiroki,

    I have pushed MockActiveRelation to github (as MockRelation).

    https://github.com/BenEddy/mock_relation

    The license is MIT.

  3. 3
    Reply

    irohiroki

    on March 9, 2014 said:

    Great, thank you!

  4. 4
    Reply

    Chip

    on April 10, 2014 said:

    Powerful stuff. It’s impressive how only a sprinkle of code can solve my AR testing issues. Thank you for this!

Leave a Comment

All fields are required unless marked as optional.

Some HTML is okay.