Testing Rails: How to test Rails ActiveRecord Named Scopes

June 17th, 2009 at 9:37 am • permalink6 comments

One of my biggest deal when approaching new features in Rails is the correct answer to the question “how should I test this?“.

Someone would probably assert that everything you code can be tested and, even though I perfectly agree with that, I would probably be more interested in how that shiny piece of code can be tested. Rails made the creation of tests a piece of cake, but it’s not always so easy.

Have you ever tried to test the correct creation of a cookie in Rails 2.2? Have you ever written an helper test that involves routing rules in Rails 2.0? Have you ever created a test to ensure your caching strategy works as expected?

If you answered yes to at least to one of those questions, you probably know what I mean.

The first requirement to write effective tests is to know what you are playing with. You can’t really know how to test an ActiveRecord callback unless you don’t know how callbacks works in ActiveRecord and, off course, what your hook is supposed to do.

The second suggestion if you get stuck writing a test is to start reading how that feature or the underlying implementation has been tested in the original framework. So, if you need to write a test for your custom Rails foo_tag helper, you probably want to start reading how the Rails team tested the helpers available with the Rails framework. Believe me, you’ll discover tons of new features just reading the tests.

I don’t want to go deep further on how to write effective tests, I’m already writing something special about that topic. This time, let me show you how to test one of the most recent ActiveRecord features: named_scope.

What are named_scope(s)?

Named scopes have been introduced in Rails 2.1. Ok, this is probably not the very latest feature, but one of those I have been testing in the “wrong” way since a few weeks ago.

Named scopes enables you to define custom ActiveRecord filters to narrow down a database query without explicitly pass conditions or query options. You can also chain multiple scopes to get all the records matching the sum of all the conditions merged altogether.

Look the following example, taken from the great Ryan’s What’s New in Edge Rails: Has Finder Functionality blog post.

class User < ActiveRecord::Base
  named_scope :active, :conditions => {:active => true}
  named_scope :inactive, :conditions => {:active => false}
  named_scope :recent, lambda { { :conditions => ['created_at > ?', 1.week.ago] } }
end

# Standard usage
User.active    # same as User.find(:all, :conditions => {:active => true})
User.inactive # same as User.find(:all, :conditions => {:active => false})
User.recent   # same as User.find(:all, :conditions => ['created_at > ?', 1.week.ago])

How to test named_scope(s) (the wrong way)

Now that you know what named scopes are and how they works, at least on the surface, you probably want to know how to test them.

When I first started writing named scope tests, the most obvious way seemed to me to use fixtures. Unfortunately, my fixture files started to be cluttered by fake definitions quickly enough. Tons of records required just to test a single method.

# The following fixtures are used to test the Airport #best named_scope.
# The value for search doesn't match current number of search instances
# but it doesn't really matter in this case.

<% 10.times do |index| %>
initial_airport_code_ROM_<%= index %>:
  search: search_hotels_example
  custom_field: initial_airport_code
  value: ROM
<% end %>

<% 5.times do |index| %>
initial_airport_code_FCO_<%= index %>:
  search: search_hotels_example
  custom_field: initial_airport_code
  value: FCO
<% end %>

<% 5.times do |index| %>
initial_airport_code_RMA_<%= index %>:
  search: search_hotels_example
  custom_field: initial_airport_code
  value: RMA
<% end %>

Even worse, my code started to be overwhelmed by ugly precondition checks in order to ensure my test didn’t pass just because of inconsistent fixtures.

def test_recent_should_returns_latest_N_records_ordered_by_created_at
  # precondition: ensure the test doesn't pass
  # just because there are only 3 users.
  assert User.count > 3

  users = User.recent(3).all
  assert_equal(3, users.length)
  assert_equal(User.find(:all, :limit => 3, :order => 'created_at DESC'), users)
end

Needless to say, the work to maintain all the fixtures was driving me crazy. I couldn’t believe that was the right way.

Then comes Factory, and developers started to say factories are better than fixtures. I agree, at least you don’t have to deal with messy fixtures.

At this point I quickly decided to change my tests from the old fixture-school to the fashionable factory-style.

test "named_scope :latest should return all bookmarks ordered by create_at DESC" do
  Bookmark.delete_all
  bookmarks = (0..3).map { |t| Factory(:bookmark, :created_at => t.months.from_now) }
  assert_equal bookmarks.reverse, Bookmark.latest
end

test "named scope :localized" do
  Tutorial.delete_all
  one = Factory(:tutorial, :language => languages(:italian))
  two = Factory(:tutorial, :language => languages(:english))
  thr = Factory(:tutorial, :language => languages(:italian))

  assert_equal([one, thr], Tutorial.localized(languages(:italian)).all)
end

test "named scope :published" do
  Tutorial.delete_all
  one = Factory(:tutorial, :published => true)
  two = Factory(:tutorial, :published => false)
  thr = Factory(:tutorial, :published => true)

  assert_equal([one, thr], Tutorial.published)
end

Yeah, this code started to smell less than the previous one. Do you agree?

But there’s still something wrong with those tests. Ok they works and they are definitely better than the previous ones or than no tests at all. But there’s still an unnoticeable smell in the air. No, it’s not you, don’t worry.

The question is: Do you really need to use real active record instances? Do you really need to test against the database? No, you don’t and you probably won’t.

How to test named_scope(s) (the right way)

There’s a better way to test named scopes. You can write your tests to test the conditions generated by the named scope instead of the records returned by the query.

Let’s say you need to test the following named scopes.

class Video < ActiveRecord::Base
  named_scope :localized, lambda { |language| { :conditions => { :language_id => language.id }}}
  named_scope :latest, lambda { |*args| { :limit => (args.shift || nil), :order => "#{self.table_name}.created_at DESC" }}
end

You already wrote these tests using the factory-way.

test "named scope :localized" do
  Video.delete_all

  one = Factory(:video, :language => languages(:italian))
  two = Factory(:video, :language => languages(:english))
  thr = Factory(:video, :language => languages(:italian))

  assert_equal([one, thr], Video.localized(languages(:italian)).all)
end

test "named_scope :latest(N) should return latest(N) bookmarks ordered by created_at DESC" do
  Video.delete_all
  videos = (0..3).map { |t| Factory(:video, :created_at => t.months.from_now) }
  assert_equal [videos[3], videos[2]], Video.latest(2)
end

test "named_scope :latest should return all bookmarks ordered by created_at DESC" do
  Video.delete_all
  videos = (0..3).map { |t| Factory(:video, :created_at => t.months.from_now) }
  assert_equal videos.reverse, Video.latest
end

You might be happy to know that there’s a really wonderful method, named proxy_options, that returns the options set by a named_scope.

p Video.latest.proxy_options
# {:limit=>2, :order=>"videos.created_at DESC"}

p Video.latest(2).proxy_options
# {:limit=>nil, :order=>"videos.created_at DESC"}

Isn’t that cool? Yeah, I know it is!

Now that we know how to get the query filters, we can use them to test our expectations.

test "named scope :localized" do
  expected = { :language_id => languages(:italian).id }
  assert_equal  expected,
                Video.localized(languages(:italian)).proxy_options
end

test "named_scope :latest(N) should return latest(N) bookmarks ordered by created_at DESC" do
  expected = { :limit => 2, :order => "videos.created_at DESC" }
  assert_equal  expected,
                Video.latest(2).proxy_options
end

test "named_scope :latest should return all bookmarks ordered by created_at DESC" do
  expected = { :limit => nil, :order => "videos.created_at DESC" }
  assert_equal  expected,
                Video.latest.proxy_options
end

I you want to make your test even more cool, I’ve found a nice addition reading the Thoughbot Pacecar unit tests. You can add an initial respond_to? assertion to make sure you actually defined the named scope before testing it.

test "named scope :localized" do
  assert Video.respond_to?(:localized)
  expected = { :language_id => languages(:italian).id }
  assert_equal  expected,
                Video.localized(languages(:italian)).proxy_options
end

Now that you know how to get access to the named scope options, should probably avoid wasting resources creating new records and running active record queries. Just because you are writing test it doesn’t mean you don’t need to care about performances or execution time.

So far, this is the best way I found to test ActiveRecord named scopes. Do you know a better one?

  1. User profile permalinks with Ruby on Rails (and Authlogic)
  2. The Road to Rails 3: make your Rails 2.3 project more Rails 3 oriented
  3. Upgrading to Rails 3: Beware of the Object#tap pattern
  4. Upgrading Rails 2 application to Rails 3 (screencast)
  5. ActiveRecord::MultiConditions development discontinued

Filed in Programming • Tags: , , , , ,

Comments

James Adam says:

I’m not sure I prefer this – you’re only testing the implementation details of the named scope here, rather than the desired behaviour.

At the very least you’d need to ensure that somewhere there was an integration test which properly exercised the method (which happens to be a named scope right now, but may not be in the future) against a set of matching and non-matching records.

I understand the desire to keep tests speedy and decoupled, but I think you’ll struggle to use your final suite of tests to refactor the implementation later on…

Hey James,
thanks for stopping by this post.

I agree with you in part and, what I probably forgot to say in my post, it that there are some situation where it actually makes sense to run a database query.

For instance, testing the raw proxy_options can cause a test to fail just because you are not coding the test in the same way you coded the scope.

In the following case, the test will fail even though the query would return the same recordset regardless you are using strings or symbols.

class Foo < ActiveRecord::Base
  named_scope :active, :conditions => { :active => true }
end

test "named_scope :active symbol" do # won't fail
  expected = { :conditions => { :active => true } }
  assert_equal expected, Foo.active.proxy_options
end

test "named_scope :active string" do # will fail
  expected = { :conditions => { "active" => true } }
  assert_equal expected, Foo.active.proxy_options
end

test "named_scope :active replacement" do # will fail
  expected = { :conditions => ['active = ?', true] }
  assert_equal expected, Foo.active.proxy_options
end

test "named_scope :active replacement with table" do # will fail
  expected = { :conditions => ['foos.active = ?', true] }
  assert_equal expected, Foo.active.proxy_options
end

This is probably the main reason why proxy_option might not be a good candidate for tests in some cases.

An other good one is that testing proxy_options doesn’t ensure you are passing invalid SQL statements to the database. This happened to me in some circumstances where I had to deal with SQLite in development mode and PostgreSQL in production or staging environment.

But the following one, IMHO, is probably a good candidate. Generally, I would prefer testing the specific proxy options when I have fair complex named_scopes (assuming you specifically want to use a named scope and not a classic method).

class Foo < ActiveRecord::Base
    named_scope :latest, lambda { |*args| { :limit => (args.shift || nil), :order => "#{self.table_name}.created_at DESC" }}
end

In this case, you might want to setup a test to stress the different behaviors and eventually an other one to test the effective result of the query.

Many thanks for your comment. It gave me the opportunity to add some more details to the article. :)

olivernn says:

Sorry for posting on an old post, just thought I’d throw in how I test named_scopes. Instead of testing the actual methods for named_scope, which I would assume are fairly well tested, I want to make sure that a particular model has a named scope with the right conditions.

it "should have an active named_scope on status" do
    Project.active.proxy_options.should == {:conditions => {:status => "active"}, :order => 'created_at DESC'}
 end

I use rspec, but really I think the idea should be the same no matter what library for testing you use.

Peter says:

I agree with @James – you’re just testing the implementation of your proxy, which doesn’t make sense. Unit testing means testing the behaviour of the class or object as it appears to the outside world.

I did learn something neat tho – proxy_options is useful to know about. Thanks!

Phillip says:

I know it’s been many months since this post was made, but since comments are still open, I thought I’d toss in my 1/2 cent.

I agree with @James and @Peter. I’ve recently been catching myself paying too much attention to implementation and have had to back off a refocus on the intended behavior. There have been a few times in the past that I’ve implemented something one way and then come back and reworked it when business needs changed. Having a behavior-oriented test in place won’t be as brittle as an implementation-oriented and will allow you to properly refactor your code using the test as an indicator that the refactor is successful.

I agree with the respond_to? part though. I have a section in my spec for methods, delegates, and whatever else I may call in code to make sure I have defined them. Then I have another describe for the behavior. It works well for me.

Peace.

james2m says:

” setup do
@clients = 3.times.map { Invitation.make }
@advisors = 2.times.map { Invitation.make(:advisor) }
@creditors = 1.times.map { Invitation.make(:creditor) }
end

should “return only client invitations” do
assert_equal @clients, Invitation.clients
end

should “return only advisor invitations” do
assert_equal @advisors, Invitation.advisors
end

should “return only creditor invitations” do
assert_equal @creditor, Invitation.creditor
end”

Add a Comment




Follow Me
    Random Quote