Posted on

In a Ruby on Rails application, it’s not uncommon to encounter code which reaches deep into the internals and database schema of a model.

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }
end

class PeopleController < ApplicationController
  def index
    @people = Person.where(gender: Person.genders[:male])
                    .where('age >= 18')
                    .where(right_handed: false)

    respond_to(:html)
  end
end

This code has a few problems:

  • The controller has way too much knowledge of the database structure of the model. Having these details bleeding into higher layers resists change to the underlying structure.
  • The chain of method calls make it incredibly hard to test if you’re using mocks.

These are implementation details which must be encapsulated within the model. ActiveRecord scopes to the rescue!

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }

  scope :male,        -> { where(gender: 1) }
  scope :adult,       -> { where('age >= 18') }
  scope :left_handed, -> { where(right_handed: false) }
end

class PeopleController < ApplicationController
  def index
    @people = Person.male.adult.left_handed

    respond_to(:html)
  end
end

The raw SQL and knowledge of model attributes has been encapsulated inside the model. Problem solved … right?

The testability has been slightly improved, but there’s still a long chain of method calls to combine the different scopes. We still have to summon an army of mocks to test our controller:

class PeopleControllerTest < ActionController::TestCase
  def test_people_index
    adult_finder        = mock
    left_handed_finder  = mock

    Person.expects(:male).returns(adult_finder)
    adult_finder.expects(:adult).returns(left_handed_finder)
    left_handed_finder.expects(:left_handed)

    get :index
    assert_response :success
  end
end

Besides expectation ridden, the test is also quite brittle. If the order of scopes changes at some point, the test fails, even though the code being tested still works.

Another problem may arise with more complex scopes. Scopes can be combined freely, but not all combinations might lead to valid SQL. Testing all combinations is very cumbersome too.

I prefer to combine the scopes into a single scope or class method inside the model, rather than chaining scopes outside the model. This keeps as much internal as possible, and allows for easier database query optimizations and other changes.

class Person < ActiveRecord::Base
  enum gender: { male: 1, female: 2 }

  scope :male,        -> { where(gender: 1) }
  scope :adult,       -> { where('age >= 18') }
  scope :left_handed, -> { where(right_handed: false) }

  class << self
    def left_handed_male_adults
      left_handed.male.adult
    end
  end
end

class PeopleController < ApplicationController
  def index
    @people = Person.left_handed_male_adults

    respond_to(:html)
  end
end

The scope chain has been wrapped inside the Person.left_handed_male_adults class method. Note that the class method could also have been defined as a scope if you wanted to. The primary difference between the two is that a scope is guaranteed to return an ActiveRecord relation.

The combined scope leads to a much simpler and more robust test:

class PeopleControllerTest < ActionController::TestCase
  def test_people_index
    Person.expects(:left_handed_male_adults)

    get :index
    assert_response :success
  end
end

By avoiding the scope chains outside of the relevant models, the codebase becomes less coupled and therefore easier to maintain and refactor.

Of course it’s still possible and very to chain the scopes since all scopes are public. Resist the urge of chaining them outside of their model to keep things simple!


If you like this post, follow me:


Or share this post: