Controller spec: testing that scope is set


#1

In a Rails controller I set the scope on a model class in an around
filter. I have defined expectations on the model classes, and ideally, I
would add a further expectation for the scope. Is this already possible
in some way? How would I go about adding support a scope expectation?

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#2

On Sun, Apr 19, 2009 at 12:27 PM, Michael S. removed_email_address@domain.invalid
wrote:

In a Rails controller I set the scope on a model class in an around
filter. I have defined expectations on the model classes, and ideally, I
would add a further expectation for the scope. Is this already possible
in some way? How would I go about adding support a scope expectation?

How are you setting the said scope?

removed_email_address@domain.invalid
http://rubyforge.org/mailman/listinfo/rspec-users


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#3

On Sunday 19 April 2009, Zach D. wrote:

On Sun, Apr 19, 2009 at 12:27 PM, Michael S.
removed_email_address@domain.invalid wrote:

In a Rails controller I set the scope on a model class in an around
filter. I have defined expectations on the model classes, and
ideally, I would add a further expectation for the scope. Is this
already possible in some way? How would I go about adding support a
scope expectation?

How are you setting the said scope?

In an around filter. However, I don’t want to test the around filter
mechanism, it might as well be rack middleware instead.

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#4

On Sun, Apr 19, 2009 at 2:09 PM, Michael S. removed_email_address@domain.invalid
wrote:

In an around filter. However, I don’t want to test the around filter
mechanism, it might as well be rack middleware instead.

Sorry, I don’t know what scope means to you in your app. Can you share
your around_filter?

removed_email_address@domain.invalid
http://rubyforge.org/mailman/listinfo/rspec-users


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#5

On Sunday 19 April 2009, Zach D. wrote:

On Sun, Apr 19, 2009 at 2:09 PM, Michael S.
removed_email_address@domain.invalid wrote:

How are you setting the said scope?

In an around filter. However, I don’t want to test the around
filter mechanism, it might as well be rack middleware instead.

Sorry, I don’t know what scope means to you in your app. Can you
share your around_filter?

Oops, sorry, I assumed the concept from ActiveRecord would be familiar.
If you know ActiveRecord::Base#with_scope that’s really all there is. A
scope, within a block or through a proxy, defines options that are
merged with the arguments to #find et al. This merging happens behind
the scenes, therefore the scoped options are effective, but don’t show
up as arguments anywhere.

I’m using this in conjunction with a generic query representation
(inspired by JSON Query) that is map through a combination of Rack
middleware and generated around_filters, see below for a glimpse.

Michael

class PeopleController < ApplicationController
include QueryScope

query_scope :only => :index do
# Only allow to filter and order by the
# virtual name attribute.
# This attribute is mapped onto the real
# firstname and lastname attributes.
allow :name
condition :name =>
“LOWER(firstname || ’ ’ || lastname) :op LOWER(?)”
order :name => “lastname :dir, firstname :dir”
end

Somewhere in QueryScope

def query_scope(options = {}, &config_block)
model_class = extract_resource!(options)
builder = QueryScopeBuilder.new(config_block)
around_filter(options) do |controller, action|
req = builder.build_request_conditioner(controller.request)
controller.instance_variable_set(:@offset_limit, req.offset_limit)
model_class.send(:with_scope, :find => req.find_options, &action)
end
end


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#6

On Sun, Apr 19, 2009 at 5:41 PM, Michael S. removed_email_address@domain.invalid
wrote:

this already possible in some way? How would I go about adding
Oops, sorry, I assumed the concept from ActiveRecord would be familiar.
It is familiar, but setting a model scope from the controller
violates the widely-accepted guideline of skinny controllers and fat
models. I’m guessing that’s why Zach wasn’t sure what you were talking
about.

Michael
allow :name
builder = QueryScopeBuilder.new(config_block)
around_filter(options) do |controller, action|
req = builder.build_request_conditioner(controller.request)
controller.instance_variable_set(:@offset_limit, req.offset_limit)
model_class.send(:with_scope, :find => req.find_options, &action)
end
end

Unless I’m mistaken, it is code like this outside models that was the
underlying motivation for adding named scopes to active record. The
reason it is problematic is that it tends to result in a lot of
duplication outside the models, and makes the controllers really hard
to understand.

Consider this alternative:

describe PeopleController do
describe “GET index” do
it “assigns a list of all people filtered by virtual name
attributes” do
people = [mock_model(Person)]
Person.stub!(:all).and_return(people)
people.should_receive(:with_virtual_names).and_return(people)
get :index
end
end
end

class PeopleController
def index
@people = PeopleController.all.with_virtual_names
end
end

Now you can specify what with_virtual_names means in a model spec.
This is easier and far less invasive than trying to specify that
specific filters are applied from the controller spec.

HTH,
David


#7

On Monday 20 April 2009, David C. wrote:

On Sun, Apr 19, 2009 at 5:41 PM, Michael S.
removed_email_address@domain.invalid wrote:

classes, and ideally, I would add a further expectation for

Oops, sorry, I assumed the concept from ActiveRecord would be
familiar.

It is familiar, but setting a model scope from the controller
violates the widely-accepted guideline of skinny controllers and fat
models. I’m guessing that’s why Zach wasn’t sure what you were
talking about.

My controllers are anorexic. The functionality I’m trying to spec is
completely generic, I just mix it into the controller.

[snip]

end

Unless I’m mistaken, it is code like this outside models that was the
underlying motivation for adding named scopes to active record. The
reason it is problematic is that it tends to result in a lot of
duplication outside the models, and makes the controllers really hard
to understand.

You are mistaken as there is no duplication at all. Among other things,
I have Rack middleware that maps request like (appropriately escaped)

/resource/?[?name=‘Dav*’][/name]

to a params hash like

{ :query => [{:attribute => ‘name’, :op => ‘=’, :target => ‘Dav*’}],
:order => [{:attribute => ‘name’}] }

This, in turn, is interpreted by a RequestConditioner (bad name) which
in this case would, with the help of some mappings passed to it, return

rc.conditions == ["(firstname || ’ ’ || lastname) LIKE ?", ‘Dav%’]
rc.order == ‘lastname, firstname’

As the last step, these pieces are used to define a scope around certain
controller actions. I could pass them explicitly to, say, #find, but
then I’d have to manually merge them with other conditions.

As to whether this functionality belongs in the model – I am against
it. What I’ve described is an adapter layer that translates from one
representation of a query to another. It is not at all related to the
core logic enclosed in the models.

end
end

class PeopleController
def index
@people = PeopleController.all.with_virtual_names
end
end

That code is sclerotic. I’m building a RIA-client that only requests
JSON-formatted data from the server. Say, I add a date of birth column
to the people grid. Then the most I want to (and have to) do is ensure
that the requisite attribute is whitelisted for querying and contained
in the response data. (Yes, I have JSON “views” with accompanying
specs.)

Taking your non-generic approach, I’d have to repeatedly write and
explicitly test, very similar code. Consider adding date of birth for
sorting and filtering to your example. Then consider writing another
controller that does roughly the same for movies with titles and release
dates. You’ll end up with repetitive code. Of course, you’re going to
factor out the repetition – and that’s where I already am.

So, long story short, I still think I could make good use of a way to
define an expectation for a specific scope in effect during a #find.
Apart from my current case, this would make it possible to check on
dynamic scopes introduced in Rails 2.3.

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#8

On Sun, Apr 19, 2009 at 6:41 PM, Michael S. removed_email_address@domain.invalid
wrote:

this already possible in some way? How would I go about adding
Oops, sorry, I assumed the concept from ActiveRecord would be familiar.
Michael
allow :name
builder = QueryScopeBuilder.new(config_block)
around_filter(options) do |controller, action|
req = builder.build_request_conditioner(controller.request)
controller.instance_variable_set(:@offset_limit, req.offset_limit)
model_class.send(:with_scope, :find => req.find_options, &action)
end
end

In your original post you asked:

“How would I go about adding support a scope expectation?”

Given the code you’ve shown it is not clear exactly what you are
expecting. Do you just want to be able to expect that you call
model_class.send(:with_scope) with the appropriate arguments?

I’m sorry if I seem dense, but you have a very clear idea of what you
want to accomplish, but it’s coming out very piece meal across
multiple emails and it’s quite difficult to pick up on,


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#9

On Sun, Apr 19, 2009 at 7:19 PM, Michael S. removed_email_address@domain.invalid
wrote:

removed_email_address@domain.invalid wrote:

req = builder.build_request_conditioner(controller.request)

This, in turn, is interpreted by a RequestConditioner (bad name) which
it. What I’ve described is an adapter layer that translates from one

  Person.stub!(:all).and_return(people)

end
sorting and filtering to your example. Then consider writing another
controller that does roughly the same for movies with titles and release
dates. You’ll end up with repetitive code. Of course, you’re going to
factor out the repetition – and that’s where I already am.

So, long story short, I still think I could make good use of a way to
define an expectation for a specific scope in effect during a #find.
Apart from my current case, this would make it possible to check on
dynamic scopes introduced in Rails 2.3.

So what does the controller’s index method actually look like?


#10

On Monday 20 April 2009, Zach D. wrote:

In your original post you asked:

“How would I go about adding support a scope expectation?”

Given the code you’ve shown it is not clear exactly what you are
expecting. Do you just want to be able to expect that you call
model_class.send(:with_scope) with the appropriate arguments?

I’d rather check that a particular scope is in effect for a call to
#find. Just as I said. :wink:

The reason is that I don’t want to encumber my controller specs with how
or where the currently effective scope was set. It might have been
multiple nested calls to with_scope or named scopes. In my current code
this is not the case, but that’s an implementation detail.

In the specs for the code that does set the scope, an expectation on the
call to with_scope is the right thing. But that’s an entirely different
spec at a lower level ob abstraction.

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#11

On Monday 20 April 2009, David C. wrote:

end

requests JSON-formatted data from the server. Say, I add a date of
course, you’re going to factor out the repetition – and that’s
where I already am.

So, long story short, I still think I could make good use of a way
to define an expectation for a specific scope in effect during a
#find. Apart from my current case, this would make it possible to
check on dynamic scopes introduced in Rails 2.3.

So what does the controller’s index method actually look like?

class PeopleController < ApplicationController

def index
respond_to do |format|
format.html { render :layout => false }
format.json do
@people = Person.all(
:offset => @offset_limit[0],
:limit => @offset_limit[1])
@count = Person.count
render
end
end
end

app/views/people/index.json.rb

{
:identifier => Person.primary_key,
:totalCount => @count,
:items => @people.map { |p|
render :partial => ‘people/item’, :locals => { :person => p }
}
}

app/views/people/_item.json.rb

{
:id => person.id,
:name => person.name,
:dob => person.date_of_birth
}

:offset and :limit are the only warts, but I can’t set them in a scope
as that would affect the total #count to, not just #find.

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#12

On 20 Apr 2009, at 02:57, Michael S. wrote:

I’d rather check that a particular scope is in effect for a call to
#find. Just as I said. :wink:

As is always the case with spec’ing Rails, I don’t know which level of
abstraction is the right one to check, but it seems like what you’re
asking for is literally:

Person.should_receive(:all) do
find_scope = Person.send(:scope, :find)
find_scope[:conditions].should == { name => [“LOWER(firstname || ’
’ || lastname) = LOWER(?)”, ‘Dav%’] }
find_scope[:order].should == { :name => “lastname ASC, firstname
ASC” }
end

However this is obviously getting your hands a bit dirty with Rails
guts – I suspect others would suggest that you just set an
expectation on with_scope and be done with it (or just load some
fixtures, let the find hit the database, and make sure you get the
right records back? presumably that’s wrong in another way, as is
traditional).

Cheers,
-Tom


#13

On Sun, Apr 19, 2009 at 6:41 PM, Michael S. removed_email_address@domain.invalid
wrote:

this already possible in some way? How would I go about adding
Oops, sorry, I assumed the concept from ActiveRecord would be familiar.
Michael
allow :name
builder = QueryScopeBuilder.new(config_block)
around_filter(options) do |controller, action|
req = builder.build_request_conditioner(controller.request)
controller.instance_variable_set(:@offset_limit, req.offset_limit)
model_class.send(:with_scope, :find => req.find_options, &action)
end
end

I think I am starting to understand what you’re after. You want to
ensure the scope defined in your query_scope configuration block in
the controller is used to set the scope on the controller’s model.
Right?

With the assumption that that is correct, I would probably refactor
how your #query_scope method works. Right now you’re implicitly going
through a QueryScopeBuilder to get a RequestConditioner, in order to
access the #find_options and #offset_limit behaviour on that
RequestConditioner. I would make your controller deal with one object,
perhaps a RequestToQueryTranslator. Your #query_scope method would
come out looking like:

def query_scope(options = {}, &config_block)
model_class = extract_resource!(options)
query = RequestToQueryTranslator.translate(controller.request,
&config_block)
around_filter(options) do |controller, action|
controller.instance_variable_set(:@offset_limit,
query.offset_limit)
model_class.send(:with_scope, :find => query.find_options,
&action)
end
end

This simplifies the #query_scope method and gives you more
implementation freedom how your query is constructed. This still
leaves something difficult to spec though, you are passing your
query_scope config_block through, and I’m guessing it is
instance_eval’d. You can’t be sure of what’s going on in that
config_block unless you actually instance_eval inside of the
appropriate object. This limits your ability to write a clean
object-level example expecting the right query is constructed because
it requires your controller to work with real dependent objects.

Two approaches that come to mind for dealing with this are to change
how your config_block works. Rather than:

query_scope :only => :index do
allow :name
condition :name =>
“LOWER(firstname || ’ ’ || lastname) :op LOWER(?)”
order :name => “lastname :dir, firstname :dir”
end

You could do:

query_scope :only => :index do |query|
query.allow :name
query.condition :name =>
“LOWER(firstname || ’ ’ || lastname) :op LOWER(?)”
query.order :name => “lastname :dir, firstname :dir”
end

Now in your spec, you can write a spec against the query_scope, by
ensuring the passed in object receives #allow, #condition and #order
with appropriate arguments. Now you don’t have instance eval the block
in some dependent object somewhere, you can simply pass the query your
RequestToQueryTranslator.translate method returns to the config_block.
This gives you the advantage of ensuring that the controller sets up
the proper query, and it allows you to spec your
RequestToQueryTranslator in isolation to ensure that given a certain
set of method calls that it builds the right find options.

The code is not as pretty I agree because you have to call methods on
the query object passed to your config block. However, the advantage
is that its much easier to spec out, and you’re able to right examples
that are clearer than the alternative (ones that ensure magic happens
with a set dependent objects).

WDYT?


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#14

On Monday 20 April 2009, Zach D. wrote:

On Sun, Apr 19, 2009 at 6:41 PM, Michael S.
removed_email_address@domain.invalid wrote:
[big snip]

I think I am starting to understand what you’re after. You want to
ensure the scope defined in your query_scope configuration block in
the controller is used to set the scope on the controller’s model.
Right?

Exactly.

query = RequestToQueryTranslator.translate(controller.request,

&config_block)
around_filter(options) do |controller, action|
controller.instance_variable_set(:@offset_limit,
query.offset_limit) model_class.send(:with_scope, :find =>
query.find_options, &action) end
end

I don’t agress. Both classes have distinct, although related purposes.
RequestCondition implements the translation from request parameters to
#find-options. QueryScope(Builder) fits it in with ActionController and
adds syntactic sugar. The former is easy to spec, for the latter, I’ve
been to lazy to figure out a good way.

WDYT?

I’ve attached the involved files. I try to avoid doing that on mailing
lists, but we’re going in circles otherwise.

In the meantime I’ve looked into rspec’s message expectation in order to
add an expectation for a particular scope being set on an ActiveRecord
class. What I’d need to implement this is a way to get hold of the
partially mocked class. There may not be one.

Michael


#15

In a functional test, create some records that will be in the scope
and some that will be out of the scope, hit the page and make sure you
only see the ones that you want. I would either do this with
cucumber, or write a controller spec and verify that only certain
records show in the the assigns var. Either way you’re going to have
to hit the db because obviously the scope affects the db queries.
This is why you’d typically like to keep that stuff in the model.

Pat


#16

On Monday 20 April 2009, Pat M. wrote:

In a functional test, create some records that will be in the scope
and some that will be out of the scope, hit the page and make sure
you only see the ones that you want. I would either do this with
cucumber, or write a controller spec and verify that only certain
records show in the the assigns var. Either way you’re going to have
to hit the db because obviously the scope affects the db queries.

Yes, and that feels a bit like cheating.

This is why you’d typically like to keep that stuff in the model.

I agree on the typically, but disagree specifically.

I’ve tried my way into rspec, see below, but got stuck at the point
where I’d have to get hold of the model class.

Michael

require ‘spec/mocks/message_expectation’

module Spec #:nodoc:
module Mocks #:nodoc:

class ScopeExpectation
  def initialize(args)
    @args = args
  end
  def scope_matches?
    # here a miracle occurs
    ...
  end
end


module MessageExpectationScopeExtension
  def self.included(base)
    base.send(:alias_method, :matches_without_scope, :matches)
    base.send(:alias_method, :matches, :matches_with_scope)
  end
  def within_scope(*args)
    @scope_expectation = ScopeExpectation.new(args)
    self
  end
  def matches_with_scope(*args, &block)
    matches_without_scope(*args, &block) &&
      (!@scope_expectation || @scope_expectation.scope_matches?)
  end
end

end
end

Spec::Mocks::MessageExpectation.class_eval do
include Spec::Mocks::MessageExpectationScopeExtension
end


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#17

On Mon, Apr 20, 2009 at 3:27 PM, Michael S. removed_email_address@domain.invalid
wrote:

Exactly.

model_class = extract_resource!(options)

#find-options. QueryScope(Builder) fits it in with ActionController and
adds syntactic sugar. The former is easy to spec, for the latter, I’ve
been to lazy to figure out a good way.

I’m just suggesting that RequestCondition and QueryScopeBuilder are
encapsulated behind RequestToQueryTranslator, so your controller
doesn’t have to know about both of them. Your controller doesn’t
actually care about request conditioners and query scope builders. It
only cares that it can translate a request to a query so it can be
used to set the scope on the model.

removed_email_address@domain.invalid
http://rubyforge.org/mailman/listinfo/rspec-users


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#18

On Mon, Apr 20, 2009 at 1:35 PM, Zach D. removed_email_address@domain.invalid
wrote:

and ideally, I would add a further expectation for the scope. Is

firstname and lastname attributes.

model_class = extract_resource!(options)
the controller is used to set the scope on the controller’s model.
def query_scope(options = {}, &config_block)
implementation freedom how your query is constructed. This still

query.allow :name
This gives you the advantage of ensuring that the controller sets up
the proper query, and it allows you to spec your
RequestToQueryTranslator in isolation to ensure that given a certain
set of method calls that it builds the right find options.

In addition to this I would still have an example that expected
with_scope to be set on the appropriate model based on the results of
query.find_options, and that @offset_limit was assigned based on the
results of query.offest_limit.


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com


#19

On Monday 20 April 2009, Zach D. wrote:

only cares that it can translate a request to a query so it can be
used to set the scope on the model.

That’s already the case. Both of these classes are only internally used
by module QueryScope, which is the only thing a controller sees:

class PeopleController < ApplicationController
include QueryScope

query_scope :only => :index do
# Only allow to filter and order by the
# virtual name attribute.
# This attribute is mapped onto the real
# firstname and lastname attributes.
allow :name
condition :name =>
“LOWER(firstname || ’ ’ || lastname) :op LOWER(?)”
order :name => “lastname :dir, firstname :dir”
end

Michael.


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#20

On Tue, Apr 21, 2009 at 4:02 AM, Michael S. removed_email_address@domain.invalid
wrote:

actually care about request conditioners and query scope builders. It
only cares that it can translate a request to a query so it can be
used to set the scope on the model.

That’s already the case. Both of these classes are only internally used
by module QueryScope, which is the only thing a controller sees:

But you’re mixing in QueryScope to the controller, it’s just like if
you put #query_scope inside of the controller itself. Mixins are a way
to organize and re-use code, but putting code in modules does not
automatically mean you are decoupling parts of your app.

Your controller spec will have to take into account whatever
dependencies your #query_scope method has unless you completely
mock/stub it out, but that seems to work against your goal,

condition :name =>


rspec-users mailing list
removed_email_address@domain.invalid
http://rubyforge.org/mailman/listinfo/rspec-users


Zach D.
http://www.continuousthinking.com
http://www.mutuallyhuman.com