Best way to test named scopes in Rails4

2019-09-10 03:18发布

问题:

As a part of the migration from Rails 3.2 to Rails 4, all named scopes need a proc block. Read more here: http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#active-record

I had missed updating a scope in one of my models, which ended up biting me in production after my migration. So I wanted to figure out how to test this issue, and I found some strange behavior.

In some cases the scopes appear to work fine without the proc, but not in other cases.

# models/offer.rb
class Offer < ActiveRecord::Base

  scope :roster, where(:on_roster => true)
  scope :commit, where("status_id > 5")

end

If I use each scope option on independent calls in rails console, the queries are built properly and the results come back as one would have expected in Rails 3.2:

$ rails c
2.0.0-p247 :001 > Offer.roster.all.size
  Offer Load (1.6ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't'
=> 1
2.0.0-p247 :002 > Offer.commit.all.size
  Offer Load (1.6ms)  SELECT "offers".* FROM "offers" WHERE (status_id > 5)
=> 3

However if I chain two scope calls together in the rails console, only the constraints from the last scope in the chain is included in each query:

2.0.0-p247 :003 > Offer.roster.commit.all.size
  Offer Load (1.4ms)  SELECT "offers".* FROM "offers" WHERE (status_id > 5)
 => 3
2.0.0-p247 :004 > Offer.commit.roster.all.size
  Offer Load (0.7ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't'
 => 1 

Now if I edit my model to add a proc to the second named scope, like so:

class Offer < ActiveRecord::Base

  scope :roster, where(:on_roster => true)
  scope :commit, -> { where("status_id > 5") }

end

If the named scope with the proc defined is at the end of the chain, it will build the query with both sets of constraints. However, if the named scope without the proc defined is at the end of the chain, the resulting query is built without the constraints of the scope with the proc defined.

$ rails c
2.0.0-p247 :003 > Offer.roster.commit.all.size
  Offer Load (1.4ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't' AND (status_id > 5)
 => 0 
2.0.0-p247 :004 > Offer.commit.roster.all.size
  Offer Load (0.7ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't'
 => 1 

So the first result is correct, and loads both scopes, but the second is incorrect and only loads the last scope. Then if you change both scopes to use procs, like so:

# models/offer.rb
class Offer < ActiveRecord::Base

  scope :roster, -> { where(:on_roster => true) }
  scope :commit, -> { where("status_id > 5") }

end

You finally get the expected behavior:

$ rails c
2.0.0-p247 :002 > Offer.roster.commit.all.size
  Offer Load (1.3ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't' AND (status_id > 5)
 => 0
2.0.0-p247 :001 > Offer.commit.roster.all.size
  Offer Load (1.7ms)  SELECT "offers".* FROM "offers" WHERE "offers"."on_roster" = 't' AND (status_id > 5)
 => 0 

One note on this, calling reload! in the rails console will not update the behavior of the scopes after you've updated and saved your model. You have to end your rails console session and begin a new one to get the proc vs. non-proc to pick up correctly.

The question I have is how to test to ensure that all of my scopes will behave as expected? Chaining scopes together each time I want to test whether they have a proc or lambda block seems very messy. However the simple tests I set up on the scopes, told me all my scopes were passing, and giving false positive results.

Is there an easy way to test via Rspec with Rails4 whether the named scope resides within a proc or lambda block?

回答1:

Scopes are only syntactic sugar for defining class methods, so by looking at the code it's quite impossible to know whether your scope was a proc/lambda or not.

Only solution I could think of is using RR and proxying the scope method. This way you could raise an exception if body does not respond to call. In your tests you than expect that no exception is raised. But I doubt this is going to work because once you set up the proxy the class has already been loaded and, thus, the scope method has been called.

I guess instead of enforcing Proc usage through test, overwriting the scope method to not allow non-procs/-lambdas at all is a better idea.

For reference: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb