JoaoHornburg
JoaoHornburg

Reputation: 907

How to mock any_instance in a scope with RSpec?

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

Answers (1)

jmonteiro
jmonteiro

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:

Option #1: Narrowing of a database query

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.

How to test scope with RSpec

This is a good answer to it (and the reasons why): Testing named scopes with RSpec.

Option #2: Executing an action on top of an instance

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.

How to test some_action with RSpec

I 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

Option #3: Mass action

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.

How to test some_action with RSpec

Same 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

Related Questions