Reputation: 907
I'm trying to write a spec which expects a method to be called on all instances in a scope. Couldn't find a elegant way to do it.
This is a simplified representation of my code:
class MyClass < ActiveRecord::Base
scope :active, where(:status => 'active')
scope :inactive, where(:status => 'inactive')
def some_action
# some code
end
This class is used by another class which calls some_action
on MyClass.all
:
class OtherClass
def other_method
MyClass.all.each do |item|
item.some_action
end
end
I want to change it to:
class OtherClass
def other_method
MyClass.active.each do |item|
item.some_action
end
end
To test such behavior I could simply MyClass.stub(:active)
, return an Array of stubs and expect some_action
on each stub. But I don't like that approach because it exposes too much details of the implementation.
What I thing would be a little more elegant is something like any_instance_in_scope
. Then I could simply write my spec as:
MyClass.any_instance_in_scope(:active).should_receive(:some_action)
Is there any way to achieve this?
Upvotes: 2
Views: 4118
Reputation: 1712
First of all, MyClass.all.some_action
isn't going to work since MyClass#some_action
is an instance method, meanwhile MyClass#all
returns an Array
-- so when you do MyClass.all.some_action
you're actually calling Array#some_action
.
Also, note MyClass.all
and MyClass.active
return different classes:
MyClass.active.class # => ActiveRecord::Relation
MyClass.active.all.class # => Array
I am not sure what your some_action
should do... Some options I imagine you might want to do:
If some_action
is filtering the array, you should convert it to be yet another scope, doing something like this:
class MyClass < ActiveRecord::Base
scope :active, where(:status => 'active')
scope :inactive, where(:status => 'inactive')
scope :some_action, ->(color_name) { where(color: color_name) }
end
And then call it using MyClass.active.some_action('red').all
. If you only want the first result, MyClass.active.some_action('red').first
.
scope
with RSpecThis is a good answer to it (and the reasons why): Testing named scopes with RSpec.
Let's say you really want to have MyClass#some_action
defined as an instance method. Then, you can try doing this:
class MyClass < ActiveRecord::Base
scope :active, where(status: 'active')
scope :inactive, where(status: 'inactive')
def some_action
self.foo = 'bar'
self
end
end
In this case, you may execute it with MyClass.active.last.some_action
, simply because #last
will return an instance, not the entire array.
some_action
with RSpecI believe you should simply test it with expectations:
MyClass.should_receive(:some_action).at_least(:once)
MyClass.active.last.some_action
Additional discussion on this: How to say any_instance should_receive any number of times in RSpec
Let's say you really want to run MyClass.active.some_action
. I'd recommend you first try this (same example as Option #2):
class MyClass < ActiveRecord::Base
scope :active, where(status: 'active')
scope :inactive, where(status: 'inactive')
def some_action
self.foo = 'bar'
self
end
end
And then run with MyClass.active.all.map{|my_class| my_class.some_action }
.
Now, if you really want to implement MyClass.active.some_action
-- you want some_action
to be executed over all instances of ActiveRecord::Relation (which I don't recommend), do this:
class MyClass < ActiveRecord::Base
scope :active, where(status: 'active')
scope :inactive, where(status: 'inactive')
def some_action
# really do it
end
end
And...
class ActiveRecord::Relation
# run some_action over all instances
def some_action
to_a.each {|object| object.some_action }.tap { reset }
end
end
Again, I don't recommend doing this.
some_action
with RSpecSame case as Option #2:
MyClass.should_receive(:some_action).at_least(:once)
MyClass.active.last.some_action
Note: All codes are using Ruby 2.0.0-p0. Install and use it, it's fun! :-)
Upvotes: 4