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.