The project I'm working on has a lot of named scopes which are really great. If you're not using them already you should really try them out. Since we test drive everything we do, we needed a really easy way to write tests for all these named scopes. We came up with a little test helper method that I thought I'd share so that other people could use it.
Here's the code:
def test_named_scope(all_objects, subset, condition)
subset.should_not be_empty
subset.each do |obj|
condition.call(obj).should be_true
end
other_objects = all_objects - subset
other_objects.should_not be_empty
other_objects.each do |obj|
condition.call(obj).should be_false
end
end
To use it, just pass a superset of objects, the subset you want to test and then a lambda as a condition. The lambda should be true for all items in the subset and false for all the items outside of it.
It sounds complicated but it's really easy! Here's an example Let's look at a simple tag class that has a status column indicating whether the tag is on a whitelist or a blacklist. It could look like this.
class Tag < ActiveRecord::Base
WHITELISTED = 1
BLACKLISTED = 0
end
We want to be able to easily grab all the whitelisted tags, so we need to add a named scope.
Here's the spec we write first:
describe Tag do
describe "whitelisted named_scope" do
it "returns the whitelisted tags" do
test_named_scope(Tag.all, Tag.whitelisted, lambda{|tag|
tag.status == Tag::WHITELISTED })
end
end
end
end
We run the spec, watch it fail and then go add the named scope to our Tag class.
class Tag < ActiveRecord::Base
WHITELISTED = 1
BLACKLISTED = 0
named_scope :whitelisted, :conditions => {:status => WHITELISTED}
end
Then we just rerun the spec and watch it pass. Easy!
Update2: Josh Susser emailed me a really nice refactoring with the enumerable partition method and Kelly fixed a bug I introduced.
def test_named_scope(all_objects, subset, condition)
scoped_objects, other_objects = all_objects.partition(&condition)
scoped_objects.should_not be_empty
other_objects.should_not be_empty
scoped_objects.should == subset
other_objects.should == all_objects - subset
end








Why a lambda and not a block?
remove
Just because I think passing multiple variables and a block looks weird: test_named_scope(Tag.all, Tag.whitelisted){|tag| tag.status == Tag::WHITELISTED }
It'd be pretty easy to rewrite it to take a block if you prefer that syntax.
remove
I've needed to do the exact same thing, but ended up with a simpler approach. Check out some code I ripped from that project. I'm sure it's obvious, but to be clear I'm using shoulda, factory_girl, and matchy.
The general idea is to verify the records returned from my named scope
Video.processedare identical to those returned fromVideo.all(:conditions => { :processed => true }). Because I know Rails will return the data I want using that query, I should simply be able to compare the two result sets and assert they're equal. If they're not, something's broken. If, for some reason, Rails is gives me unprocessed videos with that finder call, then I have much bigger problems.remove
Makes sense Larry, but a lot of our named_scopes are pretty complicated. Here for example is one we're using to check if users have left feedback on a project
The spec for the named scope looks like this
remove
Ah! I knew there was a reason you chose your approach instead of the more obvious. That's quite a hairy scope. I totally understand why you need to explicitly validate the results to guarantee you're receiving the correct results. Nicely done.
remove
I really like Josh's refactoring, but it doesn't include your tests that ensure that your test data actually includes items in and out of the conditions defined by the scope. You may want to add:
and all together:
At first I thought making sure that you had items in both the scoped and non-scoped categories was a little pedantic. On second thought, the point is that you are testing scoping and if your test data doesn't actually contain objects of both types then your test may be faulty.
Cool stuff. Thanks.
remove
Ooh, good catch Kelly, I'll fix the post. Yeah, the tests to make sure both sets actually have objects in them has caught a few bugs in code and more than a few fixture issues.
remove
I'm liking this, but it needs a little work to deal with named_scopes that have limits on them.
I modified like this for a quick fix: def test_named_scope(all_objects, subset, condition, limit = 0) scoped_objects, other_objects = all_objects.partition(&condition) other_objects += scoped_objects.slice!(limit..scoped_objects.size) if limit > 1 subset.should_not be_empty scoped_objects.should == subset other_objects.should == all_objects - subset end
remove
Here's a more robust version, but it is still trading the ability to use limits for the ability to use ordering:
remove