24

What's the best way to test scopes in Rails 3. In rails 2, I would do something like:

Rspec:

it 'should have a top_level scope' do
  Category.top_level.proxy_options.should == {:conditions => {:parent_id => nil}}
end

This fails in rails 3 with a "undefined method `proxy_options' for []:ActiveRecord::Relation" error.

How are people testing that a scope is specified with the correct options? I see you could examine the arel object and might be able to make some expectations on that, but I'm not sure what the best way to do it would be.

Trey Bean
  • 905
  • 3
  • 8
  • 19

6 Answers6

38

Leaving the question of 'how-to-test' aside... here's how to achieve similar stuff in Rails3...

In Rails3 named scopes are different in that they just generate Arel relational operators. But, investigate!

If you go to your console and type:

# All the guts of arel!
Category.top_level.arel.inspect

You'll see internal parts of Arel. It's used to build up the relation, but can also be introspected for current state. You'll notice public methods like #where_clauses and such.

However, the scope itself has a lot of helpful introspection public methods that make it easier than directly accessing @arel:

# Basic stuff:
=> [:table, :primary_key, :to_sql]

# and these to check-out all parts of your relation:
=> [:includes_values, :eager_load_values, :preload_values,
    :select_values, :group_values, :order_values, :reorder_flag,
    :joins_values, :where_values, :having_values, :limit_value,
    :offset_value, :readonly_value, :create_with_value, :from_value]

# With 'where_values' you can see the whole tree of conditions:
Category.top_level.where_values.first.methods - Object.new.methods
=> [:operator, :operand1, :operand2, :left, :left=, 
    :right, :right=, :not, :or, :and, :to_sql, :each]

# You can see each condition to_sql
Category.top_level.where_values.map(&:to_sql)
=> ["`categories`.`parent_id` IS NULL"]

# More to the point, use #where_values_hash to see rails2-like :conditions hash:
Category.top_level.where_values_hash
=> {"parent_id"=>nil}

Use this last one: #where_values_hash to test scopes in a similar way to #proxy_options in Rails2....

adzdavies
  • 1,545
  • 1
  • 13
  • 13
  • 4
    `where_values_hash` doesn't seem to work for me. `where_values_hash` gives me an empty hash but `where_values` returns an array with the correct SQL. – PhilT Sep 16 '11 at 08:16
  • 7
    So... how to test rails3 scope ? An example maybe ? – fro_oo Nov 10 '11 at 15:40
  • 1
    Even though I like the answer it clearly doesn't answer the question. – André Herculano Nov 06 '14 at 22:56
  • Andre Herculano is wrong, the very last line of the post *explicitly* and correctly answers the question. Perfect answer and worked in my rails 2 -> rails 3 conversion. – farkerhaiku Nov 24 '15 at 15:47
27

Ideally your unit tests should treat models (classes) and instances thereof as black boxes. After all, it's not really the implementation you care about but the behavior of the interface.

So instead of testing that the scope is implemented in a particular way (i.e. with a particular set of conditions), try testing that it behaves correctly—that it returns instances it should and doesn't return instances it shouldn't.

describe Category do
  describe ".top_level" do
    it "should return root categories" do
      frameworks = Category.create(:name => "Frameworks")

      Category.top_level.should include(frameworks)
    end

    it "should not return child categories" do
      frameworks = Category.create(:name => "Frameworks")
      rails = Category.create(:name => "Ruby on Rails", :parent => frameworks)

      Category.top_level.should_not include(rails)
    end
  end
end

If you write your tests in this way, you'll be free to re-factor your implementations as you please without needing to modify your tests or, more importantly, without needing to worry about unknowingly breaking your application.

Ian Lesperance
  • 4,961
  • 1
  • 26
  • 28
  • 1
    I've considered that, but all those "don't test rails" voices crop up and I figured all I should test was that I did declare the scope correctly. Still, doing it the way you suggest always gives me more confidence that I've got the SQL right. Thanks. – Trey Bean Jun 30 '10 at 14:57
  • 7
    That's not testing Rails at all. The fact that you've implemented it with a named scope doesn't matter. Hell, it could be a normal class method that calls `connection.select` and builds objects by hand. Whether you use Rails helpers or not doesn't make it any less *your* method. So go right ahead and test it. – Ian Lesperance Jul 01 '10 at 03:00
  • I've got this exact problem now. Previously the tests were coded using proxy_options but that's gone so the tests are now breaking. If they'd tested the result they would still be passing. It's the same argument as that of using mocks. It can be useful in some cases (e.g. when there's no output) but it makes your tests more brittle. – PhilT Sep 16 '11 at 08:11
  • that's not a unit test, it hits the database, that's an integration test. why the hell do we want to test that rails scopes works when that's already tested in rails – fivetwentysix Apr 02 '12 at 09:03
  • 2
    I think it's because we can't be sure that the scope we write is correct. This is especially true if it involves many conditions. – lulalala Apr 05 '12 at 07:54
  • 2
    @fivetwentysix Because all we care about is that the scope returns the correct records. We don't care how that's implemented (or even whether it's a Rails scope!), so we need to hit the DB to test it. – Marnen Laibow-Koser Aug 18 '12 at 04:03
5

This is how i check them. Think of this scope :

  scope :item_type, lambda { |item_type|
    where("game_items.item_type = ?", item_type )
  } 

that gets all the game_items where item_type equals to a value(like 'Weapon') :

    it "should get a list of all possible game weapons if called like GameItem.item_type('Weapon'), with no arguments" do
        Factory(:game_item, :item_type => 'Weapon')
        Factory(:game_item, :item_type => 'Gloves')
        weapons = GameItem.item_type('Weapon')
        weapons.each { |weapon| weapon.item_type.should == 'Weapon' }
    end

I test that the weapons array holds only Weapon item_types and not something else like Gloves that are specified in the spec.

Spyros
  • 46,820
  • 25
  • 86
  • 129
  • 4
    The problem with the .each approach is that a false positive may occur: if something is wrong with the query and zero weapons are returned, the test will pass. To counter this, you can add an additional test that checks for > 0 weapons returned. – Matt De Leon Mar 12 '12 at 20:27
3

Don't know if this helps or not, but I'm looking for a solution and ran across this question.

I just did this and it works for me

it { User.nickname('hello').should == User.where(:nickname => 'hello') }
kenny
  • 31
  • 1
  • 1
    That's assuming you have records populated in the users table in a before block, right? Otherwise, both of them should always return empty relations. – Ari May 22 '13 at 15:30
1

Quickly Check the Clauses of a Scope

I agree with others here that testing the actual results you get back and ensuring they are what you expect is by far the best way to go, but a simple check to ensure that a scope is adding the correct clause can also be useful for faster tests that don't hit the database.

You can use the where_values_hash to test where conditions. Here's an example using Rspec:

it 'should have a top_level scope' do
  Category.top_level.where_values_hash.should eq {"parent_id" => nil}
end

Although the documentation is very slim and sometimes non-existent, there are similar methods for other condition-types, such as:

order_values

Category.order(:id).order_values
# => [:id]

select_values

Category.select(:id).select_values
# => [:id]

group_values

Category.group(:id).group_values
# => [:id]

having_values

Category.having(:id).having_values
# => [:id]

etc.

Default Scope

For default scopes, you have to handle them a little differently. Check this answer out for a better explanation.

Community
  • 1
  • 1
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
  • `where_values_hash` does not do what you think it is doing. It only shows where values that evaluate to `Arel::Nodes::Equality` if you are using a range like this it won't show up: `User.where(id:(1..100)).where_values_hash == {}` also if you're using something like squeel or doing >, <, != type operations, it won't show up. – Josh Nov 10 '16 at 19:46
1

FWIW, I agree with your original method (Rails 2). Creating models just for testing them makes your tests way too slow to run in continuous testing, so another approach is needed. Loving Rails 3, but definitely missing the convenience of proxy_options!

  • But testing with `proxy_options` is testing implementation, and therefore isn't a great idea (though I do this in very simple cases sometimes). You should be testing behavior, which means you need to create models that you can run your scope against. I don't find the speed to be objectionable for continuous testing, and anyway, I value accuracy far more than speed in my tests. – Marnen Laibow-Koser Aug 18 '12 at 04:07