Testing ActiveRecord Named Scopes

Dan Croak

Everyone was psyched when Nick Kallen’s has_finder plugin was added to Rails as named_scope. They’re powerful, particularly when chaining.

One disadvantage is that they are so easy to create, people want their tests to be equally concise, which is often impossible.

Two related “testing named scopes” questions have come up recently on mailing lists.

Movie recommendations: InternetFlicks

We’re going to create a movie recommendation system called InternetFlicks.

script/generate model Movie ranking:integer in_stock:boolean
script/generate model Viewing user:belongs_to movie:belongs_to

We’ll assume a User model is already created, perhaps using Clearance.

Outside-In

Say we’re working on the recommendations page for the signed in user. With RESTful routing this might be /recommendations.

We can use a simple authorization strategy:

class RecommendationsController < ApplicationController
  before_filter :authenticate

  def index
    @movies = Movie.recommended_for(current_user)
  end
end

I’m skipping a few steps here, but our goal is to determine what interface the model needs to expose.

Also note that we’re intentionally crossing resources (“recommendations”) in the RESTful sense with a model of a different name (“Movie”). This is something we’ve started to stress in training as many students have thought REST means they need to match controller names to models (probably because they’ve seen scaffold generation).

TDD will guide you to better habits

Use Role Suggesting Name for the test name, variable names, and method under test to describe the behavior of Movie.recommended_for.

class MovieTest < ActiveSupport::TestCase
  should "recommend 2 highest ranked, in stock movies unwatched by user" do
    user    = Factory(:user)
    top_out = Factory(:movie, :ranking => 100, :in_stock => false)
    top_in  = Factory(:movie, :ranking => 95,  :in_stock => true)
    next_in = Factory(:movie, :ranking => 90,  :in_stock => true)
    watched = Factory(:movie, :ranking => 100, :in_stock => true)
    Factory(:viewing, :user => user, :movie => watched)

    assert_equal [top_in, next_in], Movie.recommended_for(user, 2)
  end
end

This is a state-based test. We create a user and a few movies, have him or her watch a movie, exercise the method, and verify the results match the test name.

Skipping ahead again, say we’ve gone through a few red, green, refactor cycles, taking into account some edge cases, and the implementation now looks like this:

class Movie < ActiveRecord::Base
  def self.recommended_for(user, limit = 10)
    highest_ranked.in_stock.unwatched(user).limited(limit)
  end

  private

  named_scope :highest_ranked, :order => "ranking desc"
  named_scope :in_stock, :conditions => { :in_stock => true }
  named_scope :unwatched, lambda { |user|
    { :joins      => "left outer join viewings
                                    on viewings.movie_id = movies.id
                                  and viewings.user_id  = #{user.id}
                      left outer join users
                                    on users.id = viewings.id",
      :conditions => "users.id is null" }
  }
end

Cool, named_scopes helped us make the recommended_for method expressive. If the specification for it changes, it will be a joy to change.

No tests for the private methods

We don’t need to test private methods.

If an object outside of Movie needs highest_ranked or one of the other private methods, we’ll write a unit test for it, watch it error, and bring it into a public scope piece by piece.

Discarded option: should_have_named_scope

About a year ago, should_have_named_scope was introduced to Shoulda.

It was later deprecated. We never felt comfortable using it. While there’s something to be said for quick Shoulda one-liners, I don’t think there’s much of an argument for should_have_named_scope except for simple scopes that are responsible for order or limit.

Since those are usually taken care of by something like utility_scopes or Pacecar (and thus do not need to be unit tested) OR are often relegated to private status, that doesn’t leave a niche for should_have_named_scope.

If you’re still a should_have_named_scope fan, please write a test for unwatched using it. I’ll be surprised if it stands up to peer review.

Discarded option: stub_chain

Another option I’ve been using this summer, but have recently rejected is stub_chain.

module StubChainMocha
  module Object
    def stub_chain(*methods)
      while methods.length > 1 do
        stubs(methods.shift).returns(self)
      end
      stubs(methods.shift)
    end
  end
end

Object.send(:include, StubChainMocha::Object)

This approach unit tests each of the named scopes and then stub_chains the class method:

should "find 10 highest ranked movies in stock that you have not seen" do
  user = Factory(:user)
  Movie.stub_chain(:highest_ranked, :in_stock, :unwatched, :limited)

  Movie.recommended_for(user)

  assert_received(Movie, :highest_ranked)
  assert_received(Movie, :in_stock)
  assert_received(Movie, :unwatched) { |expect| expect.with(user) }
  assert_received(Movie, :limited)   { |expect| expect.with(10) }
end

This is understandable, but very tied to implementation, does not test the “integration” of the scopes (dangerous in many situations when the resulting SQL is not combined as expected), and stubs out methods on the same object (usually a smell that something needs to be refactored).

My suggested approach may often be more lines of code, but will better describe the behavior.

No chains in controllers

Some of us have started to use a rule of thumb of “no chains in controllers”. This makes the testing decisions easier, and makes the model’s public interfaces and public/private interface distinction cleaner. The end result is usually a class method that wraps (potentially private) named scopes.